# Benchmarks



The sender spec's [`Worker throughput target`](https://github.com/postel-sh/postel/blob/main/openspec/specs/sender/spec.md) and [`Send latency budget`](https://github.com/postel-sh/postel/blob/main/openspec/specs/sender/spec.md) requirements are benchmarked numbers, not aspirational ones — they're re-measured by the harness below and the spec is amended whenever reality moves.

## Reference setup [#reference-setup]

* **1 Postgres node** — `postgres:16-alpine`, started via [testcontainers](https://testcontainers.com/), no tuning beyond the image default.
* **4 in-process workers** — `InProcess({ concurrency: 4 })`, the TS reference concurrency mechanism.
* **A local mock HTTP receiver** — returns `200` immediately, so the numbers isolate the sender path rather than a receiver's processing time.

## Machine assumptions [#machine-assumptions]

Numbers below were measured with Postgres running inside a Docker Desktop VM constrained to **2 vCPUs / 2.8 GB RAM** (`docker info`), driven by a Node 20 process on the host. This is a modest, easily-reproduced machine, not dedicated benchmark hardware — the point of publishing is an honest floor, not a marketing ceiling. Re-run `mise run bench` on your own hardware; expect different absolute numbers and roughly the same shape.

## Results [#results]

### Delivery throughput [#delivery-throughput]

Sustained deliveries/sec, 4 in-process workers draining an outbox of 20,000 enqueued events against the mock receiver:

| Metric                   | Measured                                                               | Spec floor |
| ------------------------ | ---------------------------------------------------------------------- | ---------- |
| Sustained deliveries/sec | \~360–400 (author's machine); \~140–150 (GitHub Actions shared runner) | ≥ 100      |

The spec floor (100/sec) is set with headroom below the slowest CI runner observed so far — it is a contractual minimum, not the expected number.

### `send()` latency [#send-latency]

Added latency of `send()` itself, firing 10,000 concurrent calls against a single Postgres node through a bounded 20-connection pool:

| Percentile | Measured    |
| ---------- | ----------- |
| p50        | \~1.2 s     |
| p95        | \~1.4–1.5 s |
| p99        | \~1.4–1.5 s |

| Metric            | Spec ceiling |
| ----------------- | ------------ |
| p99 added latency | ≤ 2000 ms    |

A literal burst of 10,000 simultaneous calls queues through whatever connection pool it's given — at 20 connections, most of that queue is wait time, not insert time. This is expected: the number describes queuing behavior under a full-burst load, not the cost of a single unblocked insert.

## Reproduce it [#reproduce-it]

```bash
mise run bench
```

Requires Docker (for the Postgres testcontainer). Runs both the throughput and latency scenarios and prints a report; set `BENCH_JSON_OUT=<path>` to also write it as JSON, or `BENCH_LATENCY_SENDS` / `BENCH_THROUGHPUT_SENDS` to change the sample sizes.

The source lives at [`typescript/scripts/bench.mjs`](https://github.com/postel-sh/postel/blob/main/typescript/scripts/bench.mjs). The throughput floor is additionally gated in CI via a `POSTEL_PG_TESTCONTAINERS`-only test at [`typescript/packages/storage/pg/test/throughput-benchmark.test.ts`](https://github.com/postel-sh/postel/blob/main/typescript/packages/storage/pg/test/throughput-benchmark.test.ts).
