Skip to content

Benchmarks

Measured with django-vcache 3.0.0 on Python 3.14 + uvloop, inside Docker, with ~1ms simulated one-way network latency to Valkey via tc netem (local-network/managed-Valkey conditions; the qdisc delays egress only, so the effective added RTT is ~1ms).

Raw client vs redis-py and valkey-glide

bench_vs.py compares the bare I/O drivers with 128-byte values and no serialization, each client in an isolated process. Sequential single operations are RTT-bound for every client; the differences show up in CPU cost per operation and under concurrency (200 tasks — the shape of an async web app):

django-vcache redis-py (hiredis) valkey-glide 2.4 valkey-glide 2.5rc
Concurrent wall µs/op 8.3 33.9 18.5 12.5
Concurrent CPU µs/op 14.8 33.2 29.5 26.3
Sequential CPU µs/op ~150–200 ~130–240 ~355–430 ~210–260
Baseline RSS 29 MB 36 MB 47 MB 46 MB
RSS growth / 90k ops +0.3 MB +0.0 MB −2.2 MB +3.1 MB

List operations (lpush/rpop), incr, and mget behave the same as set/get — the driver has no per-command overhead differences.

Two mechanisms drive the concurrent numbers: all operations multiplex over one TCP connection, and completions are delivered through a per-event-loop pipe (loop.add_reader) so the Rust side never acquires the GIL and many completions collapse into one loop wakeup.

Django cache backend vs built-in RedisCache

Same harness, realistic serialized payloads (msgpack + zstd):

django-vcache Django RedisCache
Concurrent async wall µs/op 8.6 1,496
Sync wall µs/op ~1,190 ~1,455
Total CPU, identical workload 5.1 s 79.7 s

Django's built-in backend runs its async methods through sync_to_async thread hops, which collapses under async concurrency.

Full ASGI stack

granian + uvloop, 4 workers, 300 concurrent connections, 45s sustained. Each request performs 6 cache operations (get, get_many, set, set compressed, incr, get compressed):

django-vcache django-valkey (async) Django RedisCache
Requests/sec 3,100 1,643 464
Avg latency 96 ms 183 ms 649 ms
P99 latency 179 ms 434 ms 1,616 ms
RSS over the run plateaus ~500 MB grows linearly → 1.5 GB grows linearly → 1.7 GB
Errors 0 0 990

The memory trajectory matters as much as the throughput: vcache's RSS flattens after warmup and recovers after the load stops; the connection-pool-based backends grow for the entire run.

Rust-direct (embedded consumers)

A downstream Rust extension can call the driver's ValkeyConn directly (reusing the shared tokio runtime), bypassing Python entirely — this is how GlitchTip's ingest path uses it. crates/vcache-core/examples/bench.rs measures that ceiling:

via Python bridge Rust-direct
Concurrent wall µs/op 8.3 6.3
Concurrent CPU µs/op 14.8 4.0
Sequential CPU µs/op ~150–200 ~60–73

Reproduce

docker compose build
docker compose up -d valkey

# Raw client comparison (installs redis-py, valkey-py, valkey-glide)
docker compose run --rm app bash -c \
  "pip install -q redis hiredis 'valkey[libvalkey]' valkey-glide && \
   python bench_vs.py"

# Full ASGI stack comparison
docker compose run --rm app python bench_compare.py --duration 45 -c 300

# Rust-direct ceiling
docker compose run --rm app bash -c \
  "tc qdisc add dev eth0 root netem delay 1ms && \
   cargo run --release --example bench"