Skip to content
Get started
Get started
bunqueue v2.9.4
View Markdown
blog · release

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.

bun add bunqueue@2.9.4
npm install bunqueue@2.9.4
bun add bunqueue@latest
ContractEvidence preserved by the implementation
Wire compatibilityACKB, request/response framing, MessagePack encoding, lock tokens, result ordering, and retry commands are unchanged.
Configured batch sizeThe reachable frontier is a ceiling only: the effective threshold can shrink but never exceeds batchSize.
Native batch processingA sealed native batch contributes its exact started members, so full and partial batches coalesce without using the configured maximum as a guess.
Scalar bufferOnly current deliveries that can start within concurrency, rate, and simulated per-group capacity contribute to the pending threshold.
Outcome phasesA 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 controlsConcurrency reduction, pause, runtime rate limiting, and close immediately re-evaluate pending ACKs against the reduced frontier.
Failure handlingIn-flight flushes remain tracked, retry limits and delays are unchanged, and close() continues to await pending/in-flight acknowledgements.
Embedded workersEmbedded ACKs remain direct and do not use the TCP-only capacity ceiling.
Half-open recoveryWorkers continue to surface transient transport errors through the required error listener while the connection health path reconnects and resumes throughput.
AreaPrevious hot-path costOptimizationResulting behavior
TCP Worker ACK batchingA 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 evidenceOnce 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 retentionEvery 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 setupScalar 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 lifecycleTrim, 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 workflowCPU 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.

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.

WorkloadBeforeAfterImprovement
TCP Worker, 60 trivial jobs, concurrency=1, batchSize=1019.2795 jobs/s; 3,112.113 ms2,710.8484 jobs/s; 22.133 ms140.61x throughput; 99.29% less elapsed time
One-million-job in-memory completion phase46,356 ms2,691 ms94.2% less time; 17.2x faster
One-million-job full in-memory lifecycle48,949 ms; 20,429 jobs/s4,335 ms; 230,681 jobs/s91.1% less time; 11.3x throughput
Completion-tracker churn, 300,000 IDs with a 50,000 cap8,418.918 ms94.179 ms89.4x faster
In-memory event workload, 180,000 events across 20 queues17,746,708 retained bytes; 263,580 objects3,201,858 retained bytes; 23,649 objects82.0% fewer retained bytes; 91.0% fewer objects
SQLite scalar lifecycle, 3,000 push/pull/ACK operations2,788.07 ms1,549.35 ms44.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 scenarioEmbedded beforeEmbedded afterTCP beforeTCP after
Linear298/s399/s (+33.9%)531/s565/s (+6.4%)
Parallel275/s338/s (+22.9%)430/s464/s (+7.9%)
Compensation259/s314/s (+21.2%)434/s441/s (+1.6%)
Signal238/s284/s (+19.3%)427/s439/s (+2.8%)
InvariantRegression coverage
Recent completion evidence evicts in exact FIFO orderCovers 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 IDPer-occurrence tokens are checked before eviction and rewritten during compaction.
Event retention never exceeds the configured SQLite capCounts are loaded at startup, inserts are counted transactionally, and only the precise overflow is deleted oldest-first.
Failed SQLite writes cannot advance the cacheCount updates are applied only after the database transaction returns successfully.
Explicit trim, telemetry clear, and queue deletion remain exactEach path refreshes or invalidates its count and is exercised across subsequent writes.
maxQueueEvents=0 does not disable terminal metricsEvent rows are skipped, while completed/failed metric grouping and cumulative metadata still run.
Empty-queue cleanup does not erase cumulative metricsOnly the transient in-memory retention count is released; obliterate remains the operation that clears telemetry history.
GateResult
Full disposable-container sandboxPassed: 8,564 unit tests, 69/69 TCP suites with 499 assertions, and 43/43 embedded suites with 342 tests; zero failures.
Asynchronous lifecycle command modelPassed: 11 tests and 84,144 assertions against a real TCP broker and SQLite.
Repeated new TCP ACK regressionsPassed: 200/200 across 20 repetitions of all ten frontier scenarios.
Focused Worker, ACK, durability, and half-open campaignsPassed: 107/107 with no job loss, duplicate ACK, ordering error, or unrecovered connection.
Static verificationTypeScript 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:

SuiteDurationPeak RAMStart -> end RAMCPU avg / p95 / peakPID peakRunner verdict
Unit7.96 min1.83 GiB201.0 -> 272.9 MiB65.3% / 230.3% / 692.5%132No anomalies
TCP8.99 min166.1 MiB99.7 -> 134.3 MiB9.1% / 26.8% / 71.8%61No anomalies
Embedded4.11 min42.8 MiB27.3 -> 42.7 MiB4.1% / 19.2% / 36.0%40No 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.

Deep profiling also identified further opportunities, but they are not part of this change set and their behavior is unchanged:

CandidateObserved costWhy it remains separate
Atomic removeOnComplete completion evidenceDurable 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 coalescingDisabling 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 removalA 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() writesFull 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 countersgetJobs(), 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 replacementmsgpackr 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.
  • 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 Set iterator 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 error listener now that ACKs can expose the expected transient timeout before reconnection more quickly.

← bunqueue v2.9.3 · Release on GitHub · Commits since v2.9.3 · Full changelog · All posts