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"