Skip to content

Benchmark ​

Historical measurements

These measurements predate outbox 0.3. They do not measure the new lease heartbeats, fenced transitions, stored retry schedules, or bounded bulk inserts. Benchmark the installed release against your own PostgreSQL workload before using these values for capacity planning.

Measures the overhead added by OutboxEmitter.emit() and the end-to-end latency from emit to handler invocation.

What We Measure ​

BenchmarkDescription
A) INSERT — no outbox (baseline)Raw $transaction with a single INSERT INTO outbox_events
B) emit() — single eventOutboxEmitter.emit() in a transaction (includes toPayload() + JSON.stringify + INSERT)
C) emitMany() — 10 eventsOutboxEmitter.emitMany() with 10 events in a single transaction
D) Poll-to-dispatch — single eventEnd-to-end: PENDING → poller fetches → handler called
E) Poller throughput — 100 eventsTime for one poll() cycle to process a full batch of 100 events

Test Setup ​

  • NestJS: Test app with @nestjs/testing
  • PostgreSQL: Docker postgres, localhost
  • Iterations: 200 per scenario (configurable)
  • Warmup: 20 iterations (discarded)
  • Poller: polling.enabled: false, manually called via poller.poll()

Running Locally ​

bash
# Start PostgreSQL
docker compose up -d

# Run with defaults (200 iterations, 20 warmup)
DATABASE_URL=postgresql://test:test@localhost:5433/outbox_test \
  npx ts-node bench/outbox.bench.ts

# Custom iterations
DATABASE_URL=... npx ts-node bench/outbox.bench.ts --iterations 500 --warmup 50

Results ​

Measured on macOS, Node.js 20, PostgreSQL 16 (Docker), localhost. Your results will vary.

BenchmarkAvgP50P95P99
A) INSERT — no outbox (baseline)0.85ms0.78ms1.21ms1.52ms
B) emit() — single event in transaction0.91ms0.84ms1.28ms1.61ms
C) emitMany() — 10 events in transaction3.15ms2.98ms4.12ms5.03ms
D) Poll-to-dispatch — single event latency1.42ms1.31ms2.05ms2.68ms
E) Poller throughput — 100 events batch38.5ms36.2ms48.1ms55.3ms

Interpretation ​

Emit overhead is negligible. A single emit() (B) adds ~0.06ms over the baseline INSERT (A) — the cost of toPayload() serialization and JSON.stringify. This is the overhead the business code pays per event.

emitMany() scales linearly. 10 events (C) take ~3.15ms, or ~0.31ms per event. That historical run used a separate INSERT per event; 0.3 prevalidates and chunks bulk inserts, so this cost model is not current.

Poll-to-dispatch latency is dominated by the UPDATE query. The poller's UPDATE ... RETURNING query with FOR UPDATE SKIP LOCKED accounts for most of the ~1.42ms in (D). Handler invocation itself is sub-microsecond in this benchmark.

Throughput scales well. Processing 100 events in a single poll cycle (E) takes ~38.5ms, yielding ~2,600 events/sec. Real throughput depends on handler complexity and database latency.

MetricValue
Emit overhead per event (B - A)~0.06ms
emitMany per-event cost (C / 10)~0.31ms
Poll-to-dispatch single event~1.42ms
Batch throughput (100 events)~2,600 events/sec

Methodology ​

  • performance.now() for sub-millisecond timing
  • Unique event IDs prevent key collisions across runs
  • Table truncated between scenarios
  • Poller called manually (poll()) to isolate measurement from scheduling jitter
  • Each scenario runs sequentially (no concurrent interference)

Released under the MIT License.