SQLStreams

the messaging platform that is just Postgres

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

Benchmark

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

General 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:

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.

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.

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 -eu
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 .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" start
trap '"$postgres_bin/pg_ctl" -D "$benchmark_root/pgdata" stop -m fast' EXIT
export POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=55439
export POSTGRES_USER=scratch POSTGRES_PASSWORD=''
export GOMAXPROCS=4 GOGC=400 GOMEMLIMIT=2GiB
for 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"
done
just 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:

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 -eu
for 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.