Contents
layout: doc title: PostgreSQL cache benchmarks description: Measured pg_local_cache results with Node.js, Go and RESP on Apple M3 Max. Includes the machine, PostgreSQL CPU, memory and methodology. section: Benchmarks permalink: /docs/BENCHMARKS.html
last_modified_at: “2026-09-15”
PostgreSQL cache benchmarks
Recorded on an Apple M3 Max with PostgreSQL 16. Each comparison uses the same client, dataset and decoded row results for cached and ordinary SQL reads.
Results by client
Node.js / node-postgres: 16,616 requests/s for
64-key batches through SQL mget, versus 15,577 for prepared SQL at 64
connections. Client on macOS, PostgreSQL in Docker.
Go / pgx and RESP: 839,678 single-key requests/s through RESP, versus 253,790 for prepared SQL at 256 connections. Both client and PostgreSQL inside the Docker VM.
The pages include batch scaling and server resource costs. These are separate setups, not a ranking of languages. For connection examples, see Node.js, Go or RESP.
Test environment
| Component | Configuration |
|---|---|
| Host | MacBook Pro Mac15,10, Apple M3 Max: 10 performance + 4 efficiency cores, 36 GiB RAM |
| OS | macOS 26.5.2, build 25F84, arm64 |
| Docker VM | Engine 29.7.2, Linux 7.0.12-linuxkit, 14 CPUs, 7.65 GiB RAM; no container CPU or RAM quota |
| PostgreSQL | 16.15, Debian bookworm; 300 connections, 128 MiB shared buffers, 256 MiB /dev/shm |
| Data | 4,096 rows, 128-byte values; 1,024 cache entries; data and WAL on tmpfs |
The client and server share the Mac’s CPUs with ten other development containers. All clients encode requests and decode complete JSON rows, preserving input order, duplicates and missing positions. SQL uses prepared statements; connections, authentication and warmup are outside the timing. There is no TLS or pipelining.
Measurement method
Each connection waits for its response before sending another request:
a closed-loop workload, without correction for coordinated omission.
Query order rotates between repetitions; in-flight requests finish before
timing stops. Read-only comparisons use fixed keys already in the cache.
Recorded SQL plans use items_pkey, with zero shared-block reads.
Server CPU comes from the PostgreSQL container’s cgroup counters. One core means one CPU-second per elapsed second; capacity percentages divide by 14. CPU µs/request divides server CPU time by completed requests. The sampling window includes monitoring work and the short client-reporting gap.
Memory is cgroup memory.current, sampled every 500 ms plus endpoints.
Tables report the median of each repetition’s sampled peak, including shared
memory, tmpfs and page cache; this is not process RSS. JSON files also contain
block I/O, throttling, memory events and SQL state/wait snapshots. Network
counters exclude loopback and therefore omit the VM client’s traffic.
These short, warm-cache runs on a shared laptop are not production-capacity
estimates. Data and WAL use tmpfs with fsync, full_page_writes and
synchronous_commit enabled; disk performance is untested. Use your own
row sizes, query projections, key distribution and write rate for sizing.
A failed run exits nonzero and preserves completed samples; the Markdown
renderer rejects partial results. For exact recorded code, use each JSON’s
harness_ref and extension_ref. Reproduction commands are on the client
pages; results are written to JSON files such as benchmark.json.
To report another workload, attach its JSON to a workload report.