A comprehensive, fair benchmark comparing BullMQ Elixir (Redis-backed) and Oban (PostgreSQL-backed) job queues.
This benchmark compares two popular Elixir job queue libraries with proper methodology:
| Feature | BullMQ Elixir | Oban |
|---|---|---|
| Backend | Redis (in-memory) | PostgreSQL/MySQL/SQLite |
| Persistence | Redis persistence (RDB/AOF) | ACID compliant |
| Transactions | No | Yes (database transactions) |
| Architecture | Event-driven, Lua scripts | Polling-based, SQL queries |
- PostgreSQL pool_size: 150 - Exceeds concurrency to avoid connection bottlenecks
- BullMQ Redis pool_size: matches concurrency - Fair comparison without connection starvation
- Multiple runs with full statistics - Mean, median, std-dev, min, max, P95, P99
- Realistic throughput validation - Numbers are validated against theoretical limits
- Sustained workload option - Test performance over minutes, not just seconds
- Elixir >= 1.15
- Redis running on localhost:6379
- PostgreSQL running on localhost:5432
# Clone the repository
git clone https://github.com/taskforcesh/bullmq-elixir-bench.git
cd bullmq-elixir-bench/bullmq_oban_bench
# Install dependencies and set up database
mix setup
# Run the quick benchmark (sanity check)
mix bench.quick
# Run the full comparison benchmark
mix bench.comparemix bench.quickRuns a fast benchmark with minimal job counts to verify setup is working.
mix bench.compareRuns comprehensive tests including:
- Single job insertion
- Bulk job insertion
- Job processing (simulated I/O)
- CPU-intensive processing
mix bench.bulkTests bulk insertion at various job counts to analyze scaling.
# Larger job counts
mix bench.compare --jobs 50000 --process-jobs 10000
# Different concurrency
mix bench.compare --concurrency 200
# More runs for statistical significance
mix bench.compare --runs 5
# Sustained workload test (60 seconds)
mix bench.compare --sustained-duration 60
# Run specific test categories
mix bench.compare --only processing
mix bench.compare --only pure-overhead
# All options
mix bench.compare \
--jobs 20000 \
--process-jobs 10000 \
--concurrency 100 \
--job-duration 10 \
--runs 5 \
--sustained-duration 60| Option | Default | Description |
|---|---|---|
--jobs |
10000 | Number of jobs for insertion tests |
--process-jobs |
50000 | Number of jobs for processing tests |
--concurrency |
100 | Worker concurrency level |
--job-duration |
10 | Simulated job duration in ms |
--runs |
5 | Number of benchmark runs (full statistics collected) |
--sustained-duration |
disabled | Run sustained workload test for N seconds |
--only |
all | Run only specific tests (insert, processing, pure-overhead) |
Measures the throughput of inserting jobs one at a time sequentially.
- BullMQ:
Queue.add/4 - Oban:
Oban.insert!/1
Measures throughput with multiple concurrent inserters (concurrency=10).
Measures the throughput of inserting jobs in batches of 1000.
- BullMQ:
Queue.add_bulk/3 - Oban:
Oban.insert_all/1
Measures bulk throughput with multiple concurrent inserters.
Measures job processing throughput with simulated I/O work.
- Tests with both 10 and 100 workers
- Theoretical max: workers × (1000ms / job_duration) jobs/sec
- With 100 workers @ 10ms: theoretical max = 10,000 jobs/sec
Measures job processing throughput with CPU-bound work (1000 sin/cos calculations).
Measures raw queue throughput with minimal work (just counter increment). This shows the upper bound of what each queue can handle.
Tests performance under continuous load for a configurable duration.
Use --sustained-duration N to enable (N = seconds).
Configure via environment variables:
# PostgreSQL
export POSTGRES_DB=bullmq_oban_bench_dev
export POSTGRES_USER=postgres
export POSTGRES_PASSWORD=postgres
export POSTGRES_HOST=localhost
export POSTGRES_PORT=5432
# Redis
export REDIS_HOST=localhost
export REDIS_PORT=6379╔═══════════════════════════════════════════════════════════════════════════════╗
║ BENCHMARK SUMMARY ║
║ (Statistics from 5 runs per test) ║
╚═══════════════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────┬───────────────────┬───────────────────┬────────────────┐
│ Test │ BullMQ (Redis) │ Oban (PostgreSQL) │ Difference │
├─────────────────────────────────┼───────────────────┼───────────────────┼────────────────┤
│ Single Job Insert (5000) │ 4.4K jobs/sec │ 2.1K jobs/sec │ +116% │
│ Concurrent Insert (5000, c=10) │ 18.5K jobs/sec │ 9.4K jobs/sec │ +96% │
│ Bulk Insert (5000) │ 37.5K jobs/sec │ 35.9K jobs/sec │ +4.5% │
│ Concurrent Bulk (5000, c=10) │ 40.0K jobs/sec │ 41.2K jobs/sec │ -2.9% │
│ 10ms work (5000, w=10) │ 866 jobs/sec │ 500 jobs/sec │ +73% │
│ 10ms work (5000, w=100) │ 8.2K jobs/sec │ 3.4K jobs/sec │ +140% │
│ CPU work (5000, w=100) │ 24.5K jobs/sec │ 4.0K jobs/sec │ +512% │
│ Pure overhead (5000, w=100) │ 24.4K jobs/sec │ 4.3K jobs/sec │ +466% │
└─────────────────────────────────┴───────────────────┴───────────────────┴────────────────┘
DETAILED STATISTICS for 10ms work (100 workers):
┌────────────┬───────────────────────┬───────────────────────┐
│ Metric │ BullMQ │ Oban │
├────────────┼───────────────────────┼───────────────────────┤
│ Mean │ 8.2K │ 3.4K │
│ Median │ 8.2K │ 3.4K │
│ Std Dev │ 6.6 │ 962.2 │
│ Min │ 8.2K │ 2.7K │
│ Max │ 8.2K │ 4.1K │
│ P95 │ 8.2K │ 4.0K │
│ P99 │ 8.2K │ 4.1K │
└────────────┴───────────────────────┴───────────────────────┘
Notes:
• For 10ms work tests, theoretical max with 100 workers: 10,000 jobs/sec
• BullMQ achieves ~82% of theoretical max, Oban achieves ~34%
• Bulk insert is nearly identical - PostgreSQL multi-row INSERT is highly optimized
For job processing tests with simulated work:
- Theoretical maximum = workers × (1000ms / job_duration_ms)
- With 100 workers @ 10ms work: max = 100 × 100 = 10,000 jobs/sec
Results exceeding this would indicate a timing bug. Both libraries should approach but not exceed this limit.
- Positive difference: BullMQ is faster
- Negative difference: Oban is faster
-
Single/Concurrent Insertion: BullMQ is ~2x faster due to Redis in-memory operations vs PostgreSQL disk I/O.
-
Bulk Operations: Nearly identical performance! PostgreSQL's multi-row INSERT is highly optimized. For bulk inserts, Oban may even be slightly faster due to a single SQL statement vs multiple Redis commands.
-
Processing with I/O Work: BullMQ achieves ~82% of theoretical max while Oban achieves ~34%. The difference comes from BullMQ's poll-free architecture (blocking Redis commands) vs Oban's polling approach.
-
Pure Overhead: When jobs do minimal work, BullMQ can process 24K+ jobs/sec vs Oban's 4-5K. This represents the upper bound of each queue's throughput.
These benchmarks use carefully tuned configurations:
- PostgreSQL pool_size: 150 - Prevents connection contention with 100 workers
- BullMQ pool_size: 100 - Matches concurrency to avoid Redis connection bottlenecks
With default pool sizes (e.g., pool_size: 10), both libraries would show artificially lower performance due to connection starvation.
| Consideration | BullMQ (Redis) | Oban (PostgreSQL) |
|---|---|---|
| Speed | 2-5x faster for queue operations | Sufficient for most use cases |
| Bulk Insert | ~Same performance | ~Same performance |
| Durability | Depends on Redis config (RDB/AOF) | ACID compliant by default |
| Transactions | Separate from app DB | Same DB as your app |
| Complexity | Requires Redis infrastructure | Uses existing PostgreSQL |
| Backups | Separate backup strategy | Backed up with app data |
| Ecosystem | Multi-language support (Node, Python, Elixir, PHP) | Elixir-only |
For production-realistic benchmarks, use the sustained workload option:
# Run for 60 seconds of continuous load
mix bench.compare --sustained-duration 60
# Run for 5 minutes
mix bench.compare --sustained-duration 300This continuously enqueues and processes jobs for the specified duration, providing a more realistic picture of performance under sustained load.
-
Connection pool sizing: PostgreSQL pool_size is set to 150 (exceeds 100 workers) to avoid connection contention. BullMQ's Redis pool_size is set to match concurrency.
-
Statistical rigor: Each test runs multiple times with full statistics (mean, median, std-dev, min, max, P95, P99) - not cherry-picked best results.
-
Theoretical validation: Processing benchmarks are validated against theoretical limits. With 100 workers @ 10ms work, max = 10,000 jobs/sec. Results exceeding this would indicate bugs.
-
Concurrent insertion: Benchmark includes concurrent insertion tests (not just sequential) to simulate realistic multi-producer scenarios.
-
Sustained workloads: Optional
--sustained-durationflag allows testing over minutes/hours instead of just seconds. -
Timing accuracy: Timing starts BEFORE workers begin processing, not after warm-up, ensuring accurate measurement.
If you don't have Redis and PostgreSQL installed locally:
# Start Redis
docker run -d --name redis -p 6379:6379 redis:alpine
# Start PostgreSQL
docker run -d --name postgres \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
postgres:15-alpinebullmq_oban_bench/
├── bench/
│ ├── comparison_bench.exs # Full comparison benchmark
│ ├── quick_bench.exs # Quick sanity check
│ └── bulk_bench.exs # Bulk insert analysis
├── config/
│ ├── config.exs # Main configuration
│ ├── dev.exs # Development config
│ ├── test.exs # Test config
│ └── prod.exs # Production config
├── lib/
│ └── bullmq_oban_bench/
│ ├── application.ex # Application supervisor
│ ├── repo.ex # Ecto Repo for Oban
│ └── oban_worker.ex # Oban benchmark worker
├── priv/
│ └── repo/
│ └── migrations/ # Database migrations
├── mix.exs # Project definition
└── README.md # This file
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - See LICENSE for details.