Benchmark
Edit this pageGeneral Info
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.
General Methodology
These are the goals of the benchmark tests: Be clear, accurate, and reproducible.
Clear
I don’t want you staring cross-eyed at some charts and graphs trying to interpret what they mean. Someone making things unnecessarily hard to understand normally has something to hide.
Accurate
Yes, accurate, not precise. Make sure we actually measure what we intended to measure. This is easier to mess up than it sounds.
Reproducible
Be completely transparent about how the stated results were produced, so ideally someone else can come along after me and either confirm or challenge the results.
See the default settings shared by all scenarios.
Single Stream Scenario
The idea is to get the maximum sustainable producer / consumer throughput for a single stream.
To do that a single orders stream is created. An arbitrary number of producers and consumers
can be used to find the global maximum. The one caveat being only a single consumer group.
To make sure we measure the maximum sustainable throughput we have artificially low retention and cleanup task polling. This in theory should only make the results worse and I’m okay with that.
Methodology
The sustained max throughput benchmark consists of three consecutive 30-minute runs. They each used the same frozen binary and configuration. The database was recreated between runs however the PostgreSQL instance itself was not restarted or recreated.
Each run retains its environment, effective settings, raw counters, backlog and storage samples, logs, and verdict. No run was replaced or excluded.
For more detailed information:
- Evidence directory - Run archives and configuration.
- Machine-readable results - Measurements and verdicts in JSON.
- Source snapshot - Exact source used for the benchmark.
- Build manifest - Revision, hashes, timing, and rate selection.
See the single stream settings.
Results
| Run | Produced/s | Consumed/s | Backlog slope, messages/s | Logged maintenance failures | Verdict |
|---|---|---|---|---|---|
| 1 | 74,130 | 74,129 | +5.50 | 2 | Pass |
| 2 | 68,185 | 68,188 | −0.61 | 0 | Pass |
| 3 | 66,141 | 66,152 | −2.34 | 0 | Pass |
Run 1 logged two janitor partition-drop lock timeouts and retried. Runs 2 and 3 logged none.
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.
How to Reproduce
Use Go 1.27, just, and native PostgreSQL 18 tools. Run from the repository root
with a filled .env file present.
The example below uses Homebrew’s PostgreSQL installation on macOS and the PostgreSQL configuration retained with the published results. Use PostgreSQL 18.6 to match the published server version.
Below is the reproduce script:
(set -eupostgres_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=Ccat .bench/results/published/2026-09-11/postgresql.conf \ >> "$benchmark_root/pgdata/postgresql.conf""$postgres_bin/pg_ctl" -D "$benchmark_root/pgdata" \ -l "$benchmark_root/postgres.log" starttrap '"$postgres_bin/pg_ctl" -D "$benchmark_root/pgdata" stop -m fast' EXIT
export POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=55439export POSTGRES_USER=scratch POSTGRES_PASSWORD=''export GOMAXPROCS=4 GOGC=400 GOMEMLIMIT=2GiBfor benchmark_run in 1 2 3; do export POSTGRES_DB="benchmark_$benchmark_run" "$postgres_bin/createdb" -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \ -U "$POSTGRES_USER" "$POSTGRES_DB" just bench max-throughput 1 5m 1 1 on native "$postgres_bin/dropdb" -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \ -U "$POSTGRES_USER" "$POSTGRES_DB"donejust bench-report max-throughput)This creates a dedicated native cluster, runs the current max-throughput
scenario three times with a fresh database each time, and stops the cluster
afterward. Native execution accepts one repetition per invocation, so the loop
owns database creation and cleanup. The server stays running between runs.
The arguments for just bench max-throughput are:
| Argument | Name | Value |
|---|---|---|
$1 |
Time scale | 1 |
$2 |
Drain budget | 5m |
$3 |
Repetitions | 1 |
$4 |
Consumer process replicas | 1 |
$5 |
Synchronous commit | on |
$6 |
Execution mode | native |
Multi Stream Scenario
The idea is to see how producer / consumer throughput changes as we add more streams.
To do that we run the same workload over 1, 4, and 16 streams. Each stream has
an independent fraud-scoring and analytics consumer group, so every message
is handled twice.
These are short burst measurements. Unlike the single-stream scenario, messages do not expire and scheduled vacuum is suspended.
Methodology
Each stream count ran three consecutive repetitions with a 30-second warmup and a two-minute measured hold. They each used the same frozen binary and configuration. Docker Compose recreated the database cluster between runs.
For more detailed information:
- Run ledgers - Measurements and verdicts.
- Scenario source - Workload and settings at the recorded revision.
- Compose configuration - Postgres settings and container limits.
See the multi stream settings.
Results
| Streams | Produced/s | Consumed/s, all groups | Backlog slope, messages/s | PostgreSQL CPU, cores | WAL per message |
|---|---|---|---|---|---|
| 1 | 43,027 | 37,861 | +23,482 | 3.7 | 2.2 KB |
| 4 | 42,641 | 77,892 | +2,015 | 6.1 | 2.0 KB |
| 16 | 45,558 | 83,185 | +300 | 5.7 | 2.1 KB |
Produced rates count messages written. Consumed rates count messages handled across both groups. Each message is handled twice, so keeping up requires a consumed rate roughly twice the produced rate.
The runs were back to back, and each repetition produced up to ten percent less than the one before it. These two-minute holds do not establish sustainable throughput.
How to Reproduce
Use Go 1.27, just, and Docker with Compose running. Run from the repository
root with a filled .env file present.
Below is the reproduce script:
(set -eufor benchmark_streams in 1 4 16; do just bench "multistream-unpaced-$benchmark_streams" 1 5m 3 1 on compose just bench-report "multistream-unpaced-$benchmark_streams"done)This runs each stream count three times with a fresh Compose stack for each repetition, then prints the medians.
The arguments for just bench multistream-unpaced-4 are:
| Argument | Name | Value |
|---|---|---|
$1 |
Time scale | 1 |
$2 |
Drain budget | 5m |
$3 |
Repetitions | 3 |
$4 |
Consumer process replicas | 1 |
$5 |
Synchronous commit | on |
$6 |
Execution mode | compose |
Each consumer process runs the scenario’s two instances per group. The commands
use the current checkout; the published runs used revision 52cfcb2c.
Configuration
Default Config
These are the benchmark settings shared by both scenarios, not the library defaults. Each scenario lists its additional settings below.
Host Machine
All processes run on the same MacBook Air.
| Setting | Value |
|---|---|
| CPU | Apple M4, ten cores |
| Memory | 24 GiB |
| Storage | Internal 512 GB Apple AP0512Z SSD |
| Operating system | macOS |
Postgres
| Setting | Value |
|---|---|
| PostgreSQL maximum connections | 200 |
| fsync | on |
| synchronous_commit | on |
| full_page_writes | on |
| Autovacuum | on |
SQLStreams
Producer
| Setting | Value |
|---|---|
| Processes | 1 |
| Concurrent callers per stream | 4 |
| Explicit batch size | 250 |
| Batch concurrency per stream | 4 |
| Explicit batching | Enabled |
| Automatic batching | Disabled |
| Production rate | Unpaced |
Consumer
| Setting | Value |
|---|---|
| Consumer handler concurrency | 4 |
| Claim polling | 100 ms |
| Consumer handler timeout | 30 seconds |
| Maximum retries | 3 (resolved default; exception consumers suspended) |
Consumer handlers update benchmark counters and return success. They perform no application work.
Stream
| Setting | Value |
|---|---|
| Payload | 1,000-byte JSON, identity fields and repeated padding |
| Partition size | 1,000,000 messages |
Benchmark controls
The benchmark system additionally has a few configurations:
| Setting | Value | Description |
|---|---|---|
| HandlerFailRate | 0 |
No injected consumer handler failures. |
| DisableExceptionConsumers | true |
Suspends exception consumers by setting worker targets to zero. |
| DisableMessageRecording | true |
Keeps aggregate counters instead of per-message records. |
Single Stream Config
The single-stream scenario uses the default settings with the following configuration.
Postgres
PostgreSQL 18.6 runs natively on macOS.
| Setting | Value |
|---|---|
| PostgreSQL shared buffers | 6 GiB |
| PostgreSQL work memory | 32 MiB |
| Maximum WAL size | 8 GiB |
| Minimum WAL size | 2 GiB |
| Checkpoint timeout | 5 minutes |
| Checkpoint completion target | 0.9 |
| Instrumentation | pg_stat_statements, I/O timing, WAL I/O timing |
| JIT | off |
PostgreSQL has no imposed CPU or total-memory quota.
SQLStreams
Consumer
| Setting | Value |
|---|---|
| Processes | 1 |
| Claim batch | 16,000 messages |
| Queue capacity | 64,000 messages |
Stream
| Setting | Value |
|---|---|
| Message and idempotency-key retention | 120 seconds |
| DeliveryLogMode | failures (successful deliveries are not logged) |
Manager
| Setting | Value |
|---|---|
| Janitor polling | 1 second |
| Janitor delete batch | 10,000 rows |
| Partial-sweep grace | 30 seconds |
| Cleanup timeout | 30 seconds |
| Scheduled vacuum polling | 120 seconds |
| Scheduled vacuum timeout | 60 seconds |
| Scheduled vacuum | Enabled |
| Cursor advancer polling | 1 second |
Runtime
| Setting | Value |
|---|---|
| Connection pool maximum | 8 per application process |
| GOMAXPROCS | 4 |
| GOGC | 400 |
| GOMEMLIMIT | 2 GiB |
Benchmark controls
| Setting | Value | Description |
|---|---|---|
| Warmup | 5 minutes | Initial load excluded from the reported rate. |
| Measured hold | 25 minutes | Sustained load used to calculate the reported rate. |
Multi Stream Config
The multi-stream scenario uses the default settings with the following configuration.
Postgres
PostgreSQL 18.4 runs under Docker Compose on the same MacBook Air.
| Setting | Value |
|---|---|
| CPU cap | 8 CPUs |
| Memory cap | 8 GiB |
| PostgreSQL shared buffers | 2,560 MiB |
| Maximum WAL size | 4 GiB |
| Minimum WAL size | 80 MiB |
| Checkpoint timeout | 5 minutes |
| Checkpoint completion target | 0.9 |
| Instrumentation | pg_stat_statements, I/O timing |
SQLStreams
Consumer
| Setting | Value |
|---|---|
| Processes | 1 |
| Instances per group | 2 |
| Consumer groups per stream | 2 (fraud-scoring, analytics) |
| Claim batch | 2,000 messages |
| Queue capacity | 8,000 messages |
Stream
| Setting | Value |
|---|---|
| Stream count | 1, 4, or 16 |
| Message retention | No expiry |
| Idempotency-key retention | 24 hours |
| DeliveryLogMode | all (every processing attempt is logged) |
Manager
| Setting | Value |
|---|---|
| Scheduled vacuum | Suspended |
Runtime
| Setting | Value |
|---|---|
| Producer container CPU cap | 2 CPUs |
| Consumer container CPU cap | 2 CPUs |
| Connection pool maximum | 64 per application process |
| Go runtime | Container defaults |
Benchmark controls
| Setting | Value | Description |
|---|---|---|
| Warmup | 30 seconds | Initial load excluded from the reported rate. |
| Measured hold | 2 minutes | Unpaced load used to calculate the burst ceiling. |