Skip to main content

Isolated Benchmark

This benchmark compares aerospike-py with the official aerospike Python client behind a real uvicorn ASGI process. Each endpoint returns one derived scalar (found, pred_sum, or subset_sum) to remove response-serialization cost. The measured latency therefore covers batch_read, materialization, and optional inference instead of JSON encoding.

The Production Benchmark measures the other end of the spectrum. It includes HTTP parsing, full-record marshaling, DLRM inference, and network output. Use this page to isolate the client library's contribution. Use the production benchmark for capacity planning.

Source: benchmark/ in this repo. Runs locally on podman/docker against Aerospike CE 8.1.2.1_3.

How to run

cd benchmark
make up # podman compose → aerospike:ce-8.1.2.1_3 on 127.0.0.1:18710
make seed # 10k deterministic records, 5 metadata bins + 64 float features f0..f63
make bench-all CONCURRENCY=10 DURATION=30s BATCH_SIZE=50
make report # → markdown comparison grouped per scenario

make bench starts one uvicorn process for each client and waits for /healthz. It then runs oha for the requested duration, stops the process, and continues to the next client. Each run writes meta.json beside oha.json, which lets loadtest/report.py group results by (scenario, batch_size, concurrency, python).

The Aerospike CE container mounts scripts/aerospike.template.conf and pins access-address to 127.0.0.1. aerospike-py performs full cluster discovery during connect(). Without this setting, it would reconnect to the Podman-internal address and fail. The official client uses only the seed host in this benchmark.

Real measurement is follow-up

The scenario results below come from a short, warm-cache development matrix: Python 3.11, concurrency 4, batch size 30, and three seconds per run. Use them to compare the shape of the results, not as absolute performance claims. A production-grade matrix needs 10,000–100,000 seed records, 60-second runs, and concurrency between 10 and 50.

Scenarios

s1 — pure batch_read round trip

The leanest possible measurement: receive a JSON body with {offset, batch_size}, build keys, call batch_read, return only the count of found records. No materialisation of bins downstream, no inference, no full-record serialisation.

Code path differences

client_materialise
aerospike-pyresult.found_count() — pure-Rust filter+count, no PyDict build
officialsum(1 for br in result.batch_records if br.result == 0) — Python loop over BatchRecord list

The official client always pays its asyncio.to_thread hop for the batch_read call itself; that hop is what s1 isolates most cleanly.

Smoke (py3.11, c=4, batch=30, 3s)

clientRPSavg msp95
official2383.21.682.14
aerospike-py2466.81.622.39
speedup1.04×1.04×

Smallest gap of the five scenarios — request shape is dominated by HTTP framing + a single round trip, both of which are common cost. Larger concurrency widens it (production-benchmark Env A shows 4.8× on the same DB shape without the ASGI layer).

Expected on 3.14t

Largest absolute drop on both clients — spawn_blocking_delay and event_loop_resume_delay are big shares of a short request. Ratio between clients may shrink (the production-benchmark same-workload run on 3.14t saw the 42% gap collapse to ~2ms).

What's deliberately not measured

ExcludedWhy
Response body serialisationEach endpoint returns {found: N} (or + pred_sum / subset_sum) — a single scalar derived from the records. JSON encoding of full records would add a cost both clients pay but with different key/digest shapes, inflating the official-client side asymmetrically and burying the client gap.
OTel / Prometheus / neloMeasurement noise. The parent client library exposes those features; the benchmark suite stays clean.
Docker image / k8s manifestsThe benchmark isolates the client library. Deployment, registry, and manifest work would add unrelated variance.
Bin projection (bins=[…] on the wire)Server-side optimisation, orthogonal to the client comparison. s5 measures client-side materialisation cost on the same wire payload.

For the inverse — measurement that includes all the production cost — see the Production Benchmark.


Python 3.14t — runtime swap on the same suite

Python 3.14t is the free-threaded build of CPython (GIL disabled). The expectation from the Production Benchmark's 3.14t section — where spawn_blocking_delay (234 ms → 0.12 ms) and event_loop_resume_delay (39.7 ms → ~0) collapse — should reproduce here too, but the absolute deltas will be smaller because the isolated suite already trims the GIL-bound stages downstream of batch_read.

The benchmark/ project supports the runtime swap with a single argument:

uv python install 3.14t        # one-time, ~30 MB download (managed by uv)
PYTHON=3.14t uv sync # creates a separate 3.14t venv
PYTHON=3.14t make bench-all CONCURRENCY=10 DURATION=30s BATCH_SIZE=50
make report # report.py groups runs by python version

The per-scenario expected effects are written under each scenario tab above ("Expected on 3.14t").

3.14t isolated-benchmark matrix is a TODO

The "Expected on 3.14t" lines under each scenario are extrapolations from the production-benchmark stage breakdown, not measurements. The harness supports the runtime swap; a published matrix run is tracked in Bottleneck Analysis.

Files of interest

benchmark/
├── pyproject.toml # uv project, editable parent aerospike-py
├── compose.yaml # aerospike:ce-8.1.2.1_3
├── scripts/aerospike.template.conf
├── Makefile # up · seed · serve-{py,official} · bench[-all] · report
├── src/app/
│ ├── main.py · lifespan.py · config.py · model.py · seed.py
│ ├── clients/{py_async,official_async}.py
│ └── routes/s{1..5}*.py
└── loadtest/
├── run_oha.sh · bench_matrix.sh
├── scenarios.py # id → one-line description
└── report.py