layout: doc title: Node.js benchmarks description: “Local node-postgres results on Apple M3 Max: batch reads, concurrent updates, PostgreSQL CPU and memory.” section: Benchmarks permalink: /docs/benchmarks-node.html

last_modified_at: “2026-09-16”

Node.js benchmarks

Overview · Node.js · Go and RESP

Node.js 24.18.0 with node-postgres 8.16.3. Machine and measurement method.

Measured on 14 September 2026 with extension build 67e5754, a 384 MiB cache budget and clients on macOS through Docker’s published SQL port.

At 64 keys per request, median requests/s from three 10-second samples:

Connections Prepared SQL SQL mget
4 7,023 7,528
64 15,577 16,616
256 15,459 16,574

At 64 connections, mget used 170 µs of server CPU per request, versus 286 µs for SQL. Server CPU averaged 2.80 versus 4.43 cores; sampled peak memory was 215.0 versus 205.5 MiB. Client CPU was 0.84 versus 0.87 cores.

For one key at 64 connections, SQL was faster: 55,409 versus 53,646 requests/s. The batch result does not apply to single-key reads.

Raw measurements include latency percentiles, resource samples and source revisions.

Reads mixed with writes

The Node.js application runner uses 64 connections, 50,000 requests per sample and three repetitions. Reads cycle through 128 hot rows; 5% of operations in the mixed workload update rows.

Workload Keys/request Prepared SQL requests/s mget JSON requests/s
Warm reads 1 56,104 (52,246–56,364) 52,842 (52,494–53,399)
Warm reads 16 37,391 (36,996–37,938) 38,425 (38,340–38,617)
Warm reads 64 15,178 (15,109–15,488) 16,294 (16,131–16,368)
5% updates 1 55,768 (54,273–56,527) 52,621 (51,432–53,216)
5% updates 16 38,669 (38,429–38,718) 37,604 (37,481–37,916)
5% updates 64 16,049 (16,008–16,063) 16,814 (16,771–17,229)

Values are median requests/s with minimum–maximum in parentheses. Mixed samples count reads and writes together. The JSON’s application_run includes cold-fill and write-overhead cases. Cold fill at batch 64 has only 64 latency observations, too few for a useful p99 estimate.

Query setup

SELECT array_to_json(local_cache.mget('public.items'::regclass, $1::bigint[])) AS rows;

Connections and named prepared statements are reused. JSON decoding and restoring input positions are included in request time. See the Node.js example.

Reproduce

Run the Node.js benchmarks

From the repository root, with Docker and Node.js 20+:

./examples/benchmark.sh node > node.json

Current defaults: 4/64/256 connections, 1/16/64 keys, three five-second samples per case. Node.js now runs all three paths: prepared SQL, SQL mget, and RESP MGET, inside the Docker VM. The script creates a disposable server and separate client container, records resources, then removes both. Optional overrides: CONNECTIONS, BATCHES, REPEATS, DURATION_SECONDS. Use all to include Go in the same matrix. For the recorded host-based setup, use the revisions in the measurements JSON.

For reads mixed with writes:

BATCHES=1,16,64 ./examples/benchmark.sh node-workload > benchmark.json
python3 scripts/benchmark_report.py benchmark.json

Defaults: 64 connections, 50,000 requests per sample and three repetitions. The runner resets its demo tables between samples. CLIENTS, REQUESTS, BATCHES and REPEATS are optional overrides.