bunqueue v2.9.4
Deep-profiled queue hot paths and TCP ACK latency.
Deep-profiled queue hot paths and TCP ACK latency. This change set removes the quadratic completion-evidence eviction path, stops the in-memory telemetry journal from retaining event payload graphs, reuses SQLite telemetry statements and exact retention counts, and prevents low-concurrency TCP workers from waiting for the 50 ms ACK fallback on every completion wave. The implementation was driven by Bun 1.4.0 CPU and heap profiles and preserves the existing wire format, persistence schema, scheduling rules, and public configuration defaults.
To install bunqueue v2.9.4
Section titled “To install bunqueue v2.9.4”bun add bunqueue@2.9.4npm install bunqueue@2.9.4To upgrade bunqueue
Section titled “To upgrade bunqueue”bun add bunqueue@latestTCP ACK correctness and compatibility
Section titled “TCP ACK correctness and compatibility”| Contract | Evidence preserved by the implementation |
|---|---|
| Wire compatibility | ACKB, request/response framing, MessagePack encoding, lock tokens, result ordering, and retry commands are unchanged. |
| Configured batch size | The reachable frontier is a ceiling only: the effective threshold can shrink but never exceeds batchSize. |
| Native batch processing | A sealed native batch contributes its exact started members, so full and partial batches coalesce without using the configured maximum as a guess. |
| Scalar buffer | Only current deliveries that can start within concurrency, rate, and simulated per-group capacity contribute to the pending threshold. |
| Outcome phases | A delivery generation moves atomically from unqueued to pending; failure/manual transitions retire it, and ACKs already assigned to a flush cannot inflate a later batch. |
| Dynamic controls | Concurrency reduction, pause, runtime rate limiting, and close immediately re-evaluate pending ACKs against the reduced frontier. |
| Failure handling | In-flight flushes remain tracked, retry limits and delays are unchanged, and close() continues to await pending/in-flight acknowledgements. |
| Embedded workers | Embedded ACKs remain direct and do not use the TCP-only capacity ceiling. |
| Half-open recovery | Workers continue to surface transient transport errors through the required error listener while the connection health path reconnects and resumes throughput. |
Optimization summary
Section titled “Optimization summary”| Area | Previous hot-path cost | Optimization | Resulting behavior |
|---|---|---|---|
| TCP Worker ACK batching | A worker could wait for the 50 ms fallback whenever configured capacity exceeded the outcomes that could actually reach the pending batch. | Track an event-driven frontier of pending ACKs, unqueued started generations, and immediately startable scalar buffer entries, capped by batchSize. | Full waves still coalesce, while partial/final/native-batch/rate- or group-limited cohorts flush as soon as every reachable outcome is buffered. |
| Completion evidence | Once the recent-completion cap was full, repeatedly restarting a Set iterator over deleted historical slots made sustained eviction effectively quadratic. | Track recent completion order with a head index and per-occurrence tokens, with bounded stale-slot compaction. | Exact one-at-a-time FIFO eviction is amortized O(1), including delete, pin, unpin, hydration, clear, and same-ID reuse paths. |
| In-memory event retention | Every retained lifecycle event kept its payload object graph alive and front-spliced arrays during trimming. | Retain only an exact per-queue event count; subscribers still receive the original synchronous event stream. | trimEvents() keeps its count/removal contract without retaining payloads, and empty-queue cleanup releases the count while preserving cumulative metrics. |
| SQLite telemetry setup | Scalar telemetry writes repeatedly prepared the same SQL and executed retention SQL even before a queue reached its cap. | Introduce a storage-lifetime telemetry store with cached statements, reusable transactions, and exact committed event counts. | Retention deletes run only on overflow, remove exactly the excess oldest rows, and zero event retention skips journal inserts while terminal metrics remain active. |
| SQLite telemetry lifecycle | Trim, clear, and queue deletion could invalidate any cached retention knowledge. | Refresh counts after explicit trim/count operations and invalidate them after clear or queue destruction, only after the surrounding transaction commits. | Cached counts remain aligned with durable rows across restart, trim, clear, obliterate, rollback, and later reuse of the same queue name. |
| Profiling workflow | CPU time, JavaScript retention, native allocator high-water, and profiled wall time could be conflated. | Document separate native baseline, CPU, V8 heap, Markdown heap, forced-GC, Bun JSC, process-memory, queue-memory, and mimalloc evidence. | Future investigations can distinguish CPU self/total time, retained JS objects, and native allocator behavior without treating profiled timing or RSS alone as benchmark/leak proof. |
Native performance evidence
Section titled “Native performance evidence”All timings below were collected natively on an Apple M1 Max running Bun 1.4.0. Benchmark samples used fresh processes and state; profiled runs were used only for attribution. Results describe these workloads, not a universal throughput guarantee.
| Workload | Before | After | Improvement |
|---|---|---|---|
TCP Worker, 60 trivial jobs, concurrency=1, batchSize=10 | 19.2795 jobs/s; 3,112.113 ms | 2,710.8484 jobs/s; 22.133 ms | 140.61x throughput; 99.29% less elapsed time |
| One-million-job in-memory completion phase | 46,356 ms | 2,691 ms | 94.2% less time; 17.2x faster |
| One-million-job full in-memory lifecycle | 48,949 ms; 20,429 jobs/s | 4,335 ms; 230,681 jobs/s | 91.1% less time; 11.3x throughput |
| Completion-tracker churn, 300,000 IDs with a 50,000 cap | 8,418.918 ms | 94.179 ms | 89.4x faster |
| In-memory event workload, 180,000 events across 20 queues | 17,746,708 retained bytes; 263,580 objects | 3,201,858 retained bytes; 23,649 objects | 82.0% fewer retained bytes; 91.0% fewer objects |
| SQLite scalar lifecycle, 3,000 push/pull/ACK operations | 2,788.07 ms | 1,549.35 ms | 44.4% less total time |
The final event-driven frontier’s five-process median was 2,710.8484 jobs/s;
the clean baseline’s three-process median was 19.2795 jobs/s. A fresh 1,000-job
capacity matrix produced exact ACKB widths of 1, 2, 4, and 10 at the matching
worker concurrency; a configured width of 2 remained 2 at concurrency 4,
confirming that the frontier does not enlarge user-configured batches.
The SQLite telemetry work also improved representative workflow throughput while leaving workflow state transitions unchanged:
| Workflow scenario | Embedded before | Embedded after | TCP before | TCP after |
|---|---|---|---|---|
| Linear | 298/s | 399/s (+33.9%) | 531/s | 565/s (+6.4%) |
| Parallel | 275/s | 338/s (+22.9%) | 430/s | 464/s (+7.9%) |
| Compensation | 259/s | 314/s (+21.2%) | 434/s | 441/s (+1.6%) |
| Signal | 238/s | 284/s (+19.3%) | 427/s | 439/s (+2.8%) |
Telemetry and completion invariants
Section titled “Telemetry and completion invariants”| Invariant | Regression coverage |
|---|---|
| Recent completion evidence evicts in exact FIFO order | Covers cap overflow plus pin, delete, re-add, unpin, hydrate, clear, and queue-owner deletion. |
| A stale order slot cannot evict a newer occurrence of the same job ID | Per-occurrence tokens are checked before eviction and rewritten during compaction. |
| Event retention never exceeds the configured SQLite cap | Counts are loaded at startup, inserts are counted transactionally, and only the precise overflow is deleted oldest-first. |
| Failed SQLite writes cannot advance the cache | Count updates are applied only after the database transaction returns successfully. |
| Explicit trim, telemetry clear, and queue deletion remain exact | Each path refreshes or invalidates its count and is exercised across subsequent writes. |
maxQueueEvents=0 does not disable terminal metrics | Event rows are skipped, while completed/failed metric grouping and cumulative metadata still run. |
| Empty-queue cleanup does not erase cumulative metrics | Only the transient in-memory retention count is released; obliterate remains the operation that clears telemetry history. |
Validation
Section titled “Validation”| Gate | Result |
|---|---|
| Full disposable-container sandbox | Passed: 8,564 unit tests, 69/69 TCP suites with 499 assertions, and 43/43 embedded suites with 342 tests; zero failures. |
| Asynchronous lifecycle command model | Passed: 11 tests and 84,144 assertions against a real TCP broker and SQLite. |
| Repeated new TCP ACK regressions | Passed: 200/200 across 20 repetitions of all ten frontier scenarios. |
| Focused Worker, ACK, durability, and half-open campaigns | Passed: 107/107 with no job loss, duplicate ACK, ordering error, or unrecovered connection. |
| Static verification | TypeScript typecheck, Oxlint/Oxfmt project checks, and git diff --check passed. |
The final parallel sandbox ran on Bun 1.4.0 in three disposable, network-isolated containers and reported no resource anomalies:
| Suite | Duration | Peak RAM | Start -> end RAM | CPU avg / p95 / peak | PID peak | Runner verdict |
|---|---|---|---|---|---|---|
| Unit | 7.96 min | 1.83 GiB | 201.0 -> 272.9 MiB | 65.3% / 230.3% / 692.5% | 132 | No anomalies |
| TCP | 8.99 min | 166.1 MiB | 99.7 -> 134.3 MiB | 9.1% / 26.8% / 71.8% | 61 | No anomalies |
| Embedded | 4.11 min | 42.8 MiB | 27.3 -> 42.7 MiB | 4.1% / 19.2% / 36.0% | 40 | No anomalies |
Container resource growth is still only an investigation signal, not proof of a JavaScript leak. Focused forced-GC heap profiles independently showed that the event-payload graph was removed and did not show retained JavaScript growth; peak RSS remains tracked separately from heap retention.
Deliberately deferred
Section titled “Deliberately deferred”Deep profiling also identified further opportunities, but they are not part of this change set and their behavior is unchanged:
| Candidate | Observed cost | Why it remains separate |
|---|---|---|
Atomic removeOnComplete completion evidence | Durable removal remained roughly 4.2-4.7x more expensive than retaining completed jobs in the isolated ACK benchmark. | Requires a new bounded multi-row transaction while preserving pin, sequence, pruning, rollback, and crash-recovery semantics. |
| Further SQLite telemetry coalescing | Disabling telemetry still materially reduces scalar persistence time after statement caching. | Changes write timing/durability and needs an explicit shutdown and failure contract. |
| Immediate temporal-index removal | A one-million-job run retained temporal entries until background cleanup even though jobs had completed. | Must update dequeue, retry, terminal, and recovery invariants together. |
Narrow WorkflowStore.update() writes | Full workflow persistence remained the largest workflow-specific SQLite caller. | Requires state-specific patches without changing signal, compensation, or recovery ordering. |
| Ordered job views and maintained counters | getJobs(), getStats(), and queue summaries still scale with total in-memory cardinality. | Every lifecycle transition must update new views/counters exactly once and remain model-checked. |
| MessagePack replacement | msgpackr was measurable on the TCP client but was not the dominant broker or persistence cost. | A replacement must preserve or version the wire format and be benchmarked independently on client and broker. |
Regression tests added
Section titled “Regression tests added”- Added real TCP protocol coverage for low-concurrency ACK flushing, concurrent coalescing, final scalar cohorts, runtime concurrency reduction, full and partial native batches, rate-limited admission, group-blocked and independent groups, and mixed successful/failed native-batch members.
- Added hot-path regression coverage that rejects
Setiterator restarts during completion eviction and proves FIFO behavior across every stale-slot path. - Added SQLite regression coverage proving storage-lifetime statement reuse and exact cached counts across restart, overflow, trim, clear, queue deletion, and reuse.
- Added an in-memory cleanup regression proving that transient event retention is released without deleting completed metrics.
- Hardened the half-open Worker recovery test with the required EventEmitter
errorlistener now that ACKs can expose the expected transient timeout before reconnection more quickly.
Thanks to 1 contributor!
Section titled “Thanks to 1 contributor!”← bunqueue v2.9.3 · Release on GitHub · Commits since v2.9.3 · Full changelog · All posts