Queue Drain Fixed Backlog¶
queue.drain_fixed_backlog is the first curated queue benchmark scenario for pgqrs.
It measures how fast a pre-populated queue can be drained under a sweep of:
- consumer count
- dequeue batch size
Question¶
For a fixed pre-populated backlog, how do throughput, completion time, and latency vary with consumer count and dequeue batch size?
Why This Matters¶
This scenario isolates the consumer side of the queue.
That makes it useful for:
- understanding how well a backend scales under drain pressure
- separating batch-size effects from concurrency effects
- identifying whether higher concurrency improves useful work or only adds contention
Setup¶
The PostgreSQL and SQLite curated baselines use:
- Rust executor
- release mode
prefill_jobs = 50000- compatibility profile
- variables:
consumers = [1, 2, 4]dequeue_batch_size = [1, 10, 50]
Additional backend notes on this page use smaller directional runs where the full 50000-job sweep is not yet practical:
- Turso guidance below uses a local-path
single_processrun withprefill_jobs = 200 - S3 guidance below uses a durable
single_processrun withprefill_jobs = 500 - The S3 run is routed through
LocalStack + Toxiproxywith60 msinjected latency and0 msjitter
Primary Findings¶
PostgreSQL¶
PostgreSQL scales with both consumers and batch size.
- At
batch_size = 1, throughput improves from149.5 msg/sto575.0 msg/sas consumers rise from1to4(3.85x). - At
batch_size = 10, throughput improves from1603.1 msg/sto5249.6 msg/s(3.27x). - At
batch_size = 50, throughput improves from6817.1 msg/sto20175.8 msg/s(2.96x). - At
1 consumer, increasing batch size from1to50improves throughput from149.5 msg/sto6817.1 msg/s(45.59x). - At
4 consumers, increasing batch size from1to50improves throughput from575.0 msg/sto20175.8 msg/s(35.09x).
SQLite¶
SQLite benefits strongly from larger batch sizes, but does not scale with more consumers in this scenario.
- At
batch_size = 1, throughput changes from261.0 msg/sto247.0 msg/sas consumers rise from1to4(0.95x). - At
batch_size = 10, throughput changes from2709.0 msg/sto2422.7 msg/s(0.89x). - At
batch_size = 50, throughput changes from12630.3 msg/sto11232.9 msg/s(0.89x). - At
1 consumer, increasing batch size from1to50improves throughput from261.0 msg/sto12630.3 msg/s(48.40x). - At
4 consumers, increasing batch size from1to50improves throughput from247.0 msg/sto11232.9 msg/s(45.47x).
Turso¶
The current Turso run is a directional local-path measurement, not a hosted edge-service benchmark.
Its shape is close to SQLite:
- At
batch_size = 1, throughput changes from173.0 msg/sto165.6 msg/sas consumers rise from1to4(0.96x). - At
batch_size = 10, throughput changes from1503.7 msg/sto1504.5 msg/s(1.00x). - At
batch_size = 50, throughput changes from6452.5 msg/sto6804.3 msg/s(1.05x). - At
1 consumer, increasing batch size from1to50improves throughput from173.0 msg/sto6452.5 msg/s(37.30x). - At
4 consumers, increasing batch size from1to50improves throughput from165.6 msg/sto6804.3 msg/s(41.09x).
This is the behavior to expect from the current turso:///... backend in this repo: local SQLite-family storage with a different client stack, not a remote Turso edge deployment.
S3¶
The current S3 run is a directional durable baseline over an emulated remote object-store path.
It behaves very differently from the local-file backends:
- At
batch_size = 1, throughput changes from6.6 msg/sto6.7 msg/sas consumers rise from1to4(1.01x). - At
batch_size = 10, throughput changes from66.2 msg/sto63.6 msg/s(0.96x). - At
batch_size = 50, throughput changes from325.8 msg/sto265.4 msg/s(0.81x). - At
1 consumer, increasing batch size from1to50improves throughput from6.6 msg/sto325.8 msg/s(49.35x). - At
4 consumers, increasing batch size from1to50improves throughput from6.7 msg/sto265.4 msg/s(39.89x).
This is the useful queue-drain takeaway for S3Store today:
- durable object-store latency dominates the drain path
- batching is still the main lever
- extra consumers do not help and can reduce throughput once they add contention on the shared remote-backed state
Latency Behavior¶
PostgreSQL¶
PostgreSQL latency stays comparatively flat as consumer count rises.
p95 dequeue latencyrises from8.78 msto11.16 msatbatch_size = 1(1.27x).p95 dequeue latencyrises from10.09 msto13.48 msatbatch_size = 50(1.34x).p95 archive latencyrises from1.13 msto2.64 msatbatch_size = 1(2.33x).p95 archive latencyrises from1.55 msto4.23 msatbatch_size = 50(2.73x).
SQLite¶
SQLite latency rises sharply as more consumers are added, even though throughput does not improve.
p95 dequeue latencyrises from7.13 msto21.06 msatbatch_size = 1(2.96x).p95 dequeue latencyrises from6.88 msto22.02 msatbatch_size = 50(3.20x).p95 archive latencyrises from0.15 msto14.30 msatbatch_size = 1(98.64x).p95 archive latencyrises from0.24 msto15.20 msatbatch_size = 50(64.15x).
Turso¶
The current Turso local-path run also shows a "local backend with limited write scaling" shape, but with lower absolute latencies than the published SQLite baseline:
p95 dequeue latencyrises from4.02 msto13.06 msatbatch_size = 1(3.25x).p95 dequeue latencyrises from4.94 msto14.97 msatbatch_size = 50(3.03x).p95 archive latencyrises from3.19 msto12.96 msatbatch_size = 1(4.06x).p95 archive latencyrises from4.03 msto14.02 msatbatch_size = 50(3.48x).
Treat these as directional rather than directly comparable to the 50000-job SQLite baseline, but the operational conclusion is straightforward: batch size matters much more than adding more consumers.
S3¶
The S3 durable path has much higher latency, and that latency rises almost linearly with consumer count in this scenario:
p95 dequeue latencyrises from79.68 msto306.40 msatbatch_size = 1(3.85x).p95 dequeue latencyrises from82.62 msto304.12 msatbatch_size = 50(3.68x).p95 archive latencyrises from80.01 msto306.52 msatbatch_size = 1(3.83x).p95 archive latencyrises from78.01 msto310.38 msatbatch_size = 50(3.98x).
These numbers come from the emulated S3 path described above, not a direct AWS measurement, so read them as "how this object-store-backed design behaves under a remote-latency envelope" rather than as a cloud-provider guarantee.
How To Interpret This¶
The current benchmark says:
- PostgreSQL is the backend that scales with concurrency for this queue scenario.
- SQLite is functional and predictable, with good single-consumer throughput, but extra consumers mostly add contention rather than throughput.
- Turso in this repo currently behaves like a local SQLite-family backend, not a remote edge deployment.
- S3 is viable when remote durable queue state matters more than raw throughput, but it is the slowest option in this scenario by a large margin because end-to-end per-message latency is much higher.
- Batch size is an important lever across all of the current backends.
In practical terms:
- choose PostgreSQL for multi-consumer throughput
- choose SQLite or local-path Turso for simple embedded usage
- choose S3 only when object-storage-backed durability and portability are worth the throughput tradeoff
This should be read as scenario behavior, not as a universal backend ranking.
SQLite remains useful for embedded, test, and low-operational-overhead use cases even when it is not the scalable choice for multi-consumer drain workloads.
Artifacts¶
Curated baselines used for this page:
postgres-rust-compat-release-20260321.jsonlsqlite-rust-compat-release-20260321.jsonls3-rust-single_process-baseline-20260331.jsonl
Directional run referenced for Turso:
To explore runs interactively:
Comparability Notes¶
- PostgreSQL and SQLite are the cleanest like-for-like comparison on this page because both use the same curated
prefill_jobs = 50000compatibility baseline. - The Turso results are directional local-path numbers from a smaller
prefill_jobs = 200run. - The S3 results are directional durable numbers from a smaller
prefill_jobs = 500run overLocalStack + Toxiproxy. - That means S3 and Turso guidance here is useful for backend selection and shape-of-behavior questions, but not for pretending every backend number on the page is directly apples-to-apples.