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.