Benchmarks
Published, reproducible numbers for FlexiQ against Celery, Dramatiq, RQ and BullMQ — the wins and the losses.
Published, reproducible numbers for FlexiQ against Celery, Dramatiq, RQ and BullMQ — the wins and the losses.
An unbenchmarked performance claim is worth less than no claim, because the
first reader who cares will run their own. So here is ours, with the script
that produced it: bench/.
Everything on this page is generated from a committed results artifact. Nothing is typed by hand, including the machine it ran on.
Jobs completed per secondhigher is better
No broker — FlexiQ on a local SQLite filea different deployment, not a faster entrant
bench/run.py.One scenario, run against every entrant: N jobs, a fixed payload, a fixed worker concurrency, measured to completion rather than to enqueue. The task body decodes the payload, records a latency and returns — anything heavier would measure the handler rather than the queue underneath it.
Three metrics, reported separately, because an engine that wins one can lose another:
enqueue_many and most of the others do not, so using it would measure a
feature rather than a queue.created_at and
completed_at on the job row and could answer this from the database, but the
others cannot, and a number read one way for us and another way for everyone
else is not a comparison.One producer submits as fast as it can, so the latency above is end-to-end
under a load an entrant may not keep up with. Every row in the artifact records
drain.backlog_at_submit_end: how many of its jobs were still unfinished the
moment submission stopped. Where that is a large share of the run, the
percentiles are mostly time spent queued rather than the cost of handling one
job, and the completion rate is the result to read. Every entrant received the
identical load, so the ordering holds either way.
| Entrant | Backend | Concurrency | Enqueue/s | Completed/s | Backlog | p50 | p95 | p99 | Idle CPU | Idle RSS |
|---|---|---|---|---|---|---|---|---|---|---|
| FlexiQ 2.0.0Python 3.12.12 | redis | 4 worker threads (workers=4) | 7.5 | 1.1 | 435 / 500 | 217307.73 ms | 384329.61 ms | 395772.68 ms | 0.47% | 45.7 MB |
| Celery 5.6.3Python 3.12.12 | redis | 4 prefork processes (-c 4, Celery's default pool) | 29.2 | 14.2 | 261 / 500 | 9086.83 ms | 17243.96 ms | 17976.98 ms | 0.07% | 212.3 MB |
| Dramatiq 2.2.1Python 3.12.12 | redis | 1 process x 4 threads (--processes 1 --threads 4) | 23.5 | 23.4 | 1 / 500 | 48.76 ms | 150.85 ms | 278.17 ms | 0.13% | 53.6 MB |
| RQ 2.12.0Python 3.12.12 | redis | 4 `rq worker` processes (forks a child per job — RQ's default) | 13.9 | 4.5 | 339 / 500 | 37521.51 ms | 71311.83 ms | 74303.08 ms | 0% | 131.7 MB |
| BullMQ 6.3.8Node v24.12.0 | redis | 4 concurrent jobs in one process (concurrency: 4) | 23.6 | 23.6 | 1 / 500 | 64.92 ms | 155.6 ms | 198.92 ms | 0% | 96.2 MB |
| FlexiQ 2.0.0Node v24.12.0 | redis | 4 concurrent jobs in one process (concurrency: 4) | 7.8 | 0.8 | 447 / 500 | 280714.27 ms | 520533.6 ms | 541480.8 ms | 0.47% | 94.6 MB |
| FlexiQ 2.0.0Python 3.12.12 | sqlite | 4 worker threads (workers=4) | 5417.7 | 143.3 | 484 / 500 | 1702.36 ms | 3247.31 ms | 3346.9 ms | 0.27% | 50.4 MB |
| FlexiQ 2.0.0Node v24.12.0 | sqlite | 4 concurrent jobs in one process (concurrency: 4) | 4569.4 | 772.2 | 481 / 500 | 330.89 ms | 526.82 ms | 534.44 ms | 0.2% | 99.3 MB |
Each row carries its own concurrency_model because the number does not mean
the same thing twice: four prefork processes to Celery, one process with four
threads to Dramatiq, four worker processes to RQ — which then forks a child per
job, as it does by default — and four concurrent jobs in a single process to
BullMQ and to FlexiQ's Node SDK.
Every entrant runs with its own defaults. Nothing was tuned, FlexiQ included. A benchmark that tunes one entrant is a benchmark that entrant wrote.
The two groups in the chart are kept apart deliberately. The Redis entrants share a backend and a network link, so they are comparable with each other. The SQLite rows are a different deployment — a local file against a network service — and merging them into one ranking would be exactly the sleight of hand this harness exists to avoid.
Against a Redis that is a network hop away, FlexiQ's Redis backend is the slowest entrant in the run, and not by a little.
The reason is round trips. Claiming one job costs a ZRANGEBYSCORE over the
queue's pending set, an MGET of the candidates, and then the claim script —
before the execution claim and the completion write. Dramatiq and BullMQ pop
with a single blocking command and do very little else. On a local socket that
difference is small; multiplied by a network hop it is the whole result, and it
is why those two win the comparison outright.
FlexiQ also pays on idle. Its scheduler polls — scheduler_poll_interval_ms
defaults to 50 — where RQ and BullMQ block until work arrives. That is a
fraction of a core spent doing nothing, which never shows up in a drain time
and does show up on a small instance.
The deployment FlexiQ is built for is the one in the second group — the database beside the process, no broker in the middle — and that is where the numbers in the first group would move to if the Redis were local. Neither statement is a substitute for re-running it on your own hardware, which is the whole reason the harness is in the repository.
git clone https://github.com/ByteVeda/flexiq
cd flexiq
# Plumbing only: no network, no services, about a minute.
uv run --project bench python bench/run.py --smoke
# The full comparison. Bring your own Redis.
pnpm --dir bench/node install --frozen-lockfile
REDIS_URL=redis://localhost:6379 \
uv run --project bench python bench/run.py --out bench/results/latest.jsonEvery dependency is pinned exactly, Python included, and the artifact records
the versions it ran against. See
bench/README.md
for the scenario, the metrics and the cleanup guarantees the harness makes when
it is pointed at a Redis it does not own.