SQLStreams

the messaging platform that is just Postgres

You last visited on 9999-99-99 Show what's new since then

Sustained throughput benchmark

Edit this page
Posted: 2026-09-12 · Report this thread
brandon Site Admin brandon profile Posts: 677

SQLStreams sustained approximately 68,000 messages/s produced and consumed with 1,000-byte JSON messages on an M4 MacBook Air running native PostgreSQL 18.6.

Unfortunately we live in a cruel world and these results are only ‘okay’ and not ‘nice’.

Results

Each row is one consecutive 30-minute run on September 11, 2026, using the same frozen binary and configuration. The database was recreated between runs; PostgreSQL was not restarted. Each run’s rate is the lower of producing and consuming rates, and the headline is the median of those three rates.

RunProduced/sConsumed/sBacklog slope, messages/sLogged maintenance failuresVerdict
174,13074,129+5.502Pass
268,18568,188−0.610Pass
366,14166,152−2.340Pass

All three runs passed the backlog check and finished with equal produced and handled counters: 398,139,000 messages across the full runs, including warmup. Rejected produces, unknown outcomes, and handler errors were zero. The backlog check allows a fitted slope up to 10 messages/s for unpaced runs; it does not require every instantaneous backlog sample to be zero.

Run 1 logged two janitor partition-drop lock timeouts and retried. Runs 2 and 3 logged none. Maintenance retries are reported separately from application errors; a passing verdict does not mean maintenance never retried. Log suppression can hide additional occurrences, so these are recorded counts.

Three runs show matched producing and consuming rates, temporary backlog spikes, and recurring PostgreSQL storage cleanup across their 25-minute holds.

Rates are one-minute averages from counter snapshots. Backlog and storage show the maximum observed in each minute. Backlog includes allocated messages and work still in flight. PostgreSQL storage includes data and WAL; the combined local benchmark footprint, including earlier retained artifacts, peaked at 84.62 GB against the 100 GB cap. All three run databases were dropped afterward.

Workload and evidence

The orders stream has one processor consumer group. Handlers return success without application work. Producers use explicit batches of 250 messages with four concurrent callers. Retention, idempotency-key cleanup, autovacuum, and scheduled vacuum remain enabled throughout the runs.

The evidence directory contains the machine-readable results, source snapshot, build manifest, and all three linked run archives. Each run archive retains its declaration, environment, effective settings, raw counters, backlog and storage samples, logs, and original verdict. No run was replaced or excluded. The host monitor observed no integration/test container activity or Go build/test processes during the measurements.

CPU headroom is informational for maximum-throughput runs. Crossing its 80% threshold does not fail that scenario; all three runs recorded zero crossings. Backlog and application-error checks remain required. Older verdicts retain the rules under which they were produced.

Per-message recording is disabled to avoid its JSON and disk-write overhead. Aggregate counters measure rates and totals; they do not prove that every individual message was handled exactly once. Detailed latency is unavailable in these runs. The quiet scenario retains full recording for its identity checks (0687).

Multi-stream ceiling

Streams do not contend with each other in PostgreSQL. What contends is the two consumer groups on one stream. That is the result of driving one deployment shape unpaced over 1, 4, and 16 streams on September 12, 2026: two consumer groups on every stream with a delivery-log row per delivery, explicit batches of 250 from four callers per stream, a 64-connection pool per process, and aggregate counters instead of per-message records. Each stream count ran a 30-second warmup and a two-minute unpaced hold three times from one build; the table is the median of the three holds with the full range, and every run passed.

StreamsProduced/sConsumed/s, all groupsBacklog slope, messages/sPostgreSQL CPU, coresWAL per message
143,027 (41,023–49,078)37,861 (36,320–42,908)+23,4823.7 (3.6–4.0)2.2 KB
442,641 (42,135–46,632)77,892 (77,837–83,330)+2,0156.1 (5.8–6.3)2.0 KB
1645,558 (42,574–45,994)83,185 (80,534–85,344)+3005.7 (5.7–5.8)2.1 KB

Production sits near 43,000/s at every stream count and its wall is the producer container’s two-CPU cap, which the headroom check crossed 11 to 27 times per hold on 4 and 16 streams and never on 1. Consumption is what fans out: on one stream each group manages about 19,000/s and the backlog grows by 23,000 messages every second; on four streams each group nearly doubles and on sixteen the backlog is flat. WAL per message and checkpoints (five to seven per run) stay the same from 1 stream to 16, so the database does the same work per message however the streams are cut.

These ran under Docker Compose, unlike the native setup above: PostgreSQL 18.4 capped at 8 CPUs and 8 GiB, producer and consumer containers capped at 2 CPUs each, on the same M4 host, library commit 52cfcb2c with unrelated CLI work uncommitted in the tree. The runs are back to back, and each repetition produced up to ten percent less than the one before it; the ranges carry that. The rates are two-minute burst ceilings. A sustained number needs the 25-minute shape above, and no two-minute run spans a checkpoint timeout on its own.

One number from these runs is guidance. The library’s default pool is ten connections, and a paced run with sixteen batch workers per stream on that default queued at about 4,500 produces per second however many streams it used (0767). PostgresConnectionConfig.MaxConns is the setting that moves it.

The multi-stream ledgers hold every run’s verdict, including an exploratory first run per stream count that the medians exclude. Run directories with logs, counters, and effective settings are not tracked. The current runner reproduces a stream count with just bench multistream-unpaced-4 1 5m 3 from a checkout with Docker running; just bench-report multistream-unpaced-4 prints the medians.

Configuration

All processes run on the same MacBook Air: Apple M4, ten CPU cores, 24 GiB of memory, and an internal 512 GB Apple AP0512Z SSD. PostgreSQL 18.6 runs natively on macOS; producers and consumers connect over loopback TCP.

SettingValue
Producer processes / concurrent callers1 / 4
Explicit batch size / batch concurrency250 / 4
Connection pool maximum8 per application process
Consumer processes / handler concurrency1 / 4
Consumer claim batch / queue capacity16,000 / 64,000 messages
Consumer claim polling100 ms
Payload1,000-byte JSON, identity fields and repeated padding
Message and idempotency-key retention120 seconds
Partition size1,000,000 messages
Janitor polling / delete batch1 second / 10,000 rows
Partial-sweep grace / cleanup timeout30 seconds / 30 seconds
Scheduled vacuum polling / timeout120 seconds / 60 seconds
Application runtimeGOMAXPROCS=4, GOGC=400, GOMEMLIMIT=2GiB
PostgreSQL shared buffers / work memory6 GiB / 32 MiB
Maximum / minimum WAL size8 GiB / 2 GiB
Checkpoint timeout / completion target5 minutes / 0.9
PostgreSQL maximum connections200
Instrumentationpg_stat_statements, I/O timing, WAL I/O timing
JIToff

fsync, synchronous_commit, full_page_writes, and autovacuum stay on. GOMEMLIMIT is a Go runtime target, not a process memory cap; PostgreSQL has no imposed CPU or total-memory quota. Successful deliveries do not create per-message delivery-log entries. Exception consumers are suspended, and no failures are injected.

These settings deliberately exercise retention every few minutes. They are not the library defaults or a recommendation to retain production messages for two minutes. Application work inside the handler, remote database access, additional consumer groups, and a different payload change the workload.

Reproduce the published runs

The current runner uses just bench <scenario>, with just bench-smoke for a one-minute quiet run and just bench-report <scenario> for saved results. The commands below use the frozen source that produced these published results, including its original paths and binary names.

Use PostgreSQL 18.6 and Go 1.27. Download the source archive and PostgreSQL configuration into an evidence directory outside the checkout. In a separate checkout at base revision ce33ff2cc834ad781cd68913a1acd86d6a8faafc, extract the source archive before building. It includes the benchmark changes and local Go workspace; the manifest records its SHA-256 hash.

From that checkout, run the following on macOS. It creates a dedicated native cluster, runs the same scenario three times, and stops the cluster afterward. The runner drops each run’s database and retains the result files. Its storage guard stops a run at 100 GB of cluster-plus-result storage or below 40 GiB free. The publication session additionally counted earlier local benchmark artifacts against the same 100 GB cap.

set -eu
benchmark_evidence="/path/to/downloaded/evidence"
tar -xzf "$benchmark_evidence/source.tar.gz"
postgres_bin="$(brew --prefix postgresql@18)/bin"
benchmark_root="$(mktemp -d /tmp/sqlstreams-benchmark.XXXXXX)"
"$postgres_bin/initdb" -D "$benchmark_root/pgdata" -U scratch \
  --auth=trust --encoding=UTF8 --locale=C
cat "$benchmark_evidence/postgresql.conf" \
  >> "$benchmark_root/pgdata/postgresql.conf"
"$postgres_bin/pg_ctl" -D "$benchmark_root/pgdata" \
  -l "$benchmark_root/postgres.log" start
trap '"$postgres_bin/pg_ctl" -D "$benchmark_root/pgdata" stop -m fast' EXIT

(cd .bench && go build -o "$benchmark_root/reliability" ./reliability)
export GOMAXPROCS=4 GOGC=400 GOMEMLIMIT=2GiB
for benchmark_run in 1 2 3; do
  python3 .bench/reliability/native.py \
    --binary "$benchmark_root/reliability" \
    --postgres-bin "$postgres_bin" --port 55439 --user scratch \
    --scenario max-throughput
done

Keep integration tests, other database workloads, and builds out of the measured window. The settings are tuned for this host; reproduction on another machine measures that machine’s capacity. No additional handler work is included.