Valkey started in March 2024 as a Linux Foundation fork of Redis 7.2.4, after Redis moved to source-available licenses. Two and a half years later both projects ship new major versions and still speak the same protocol, so any Redis client works against either. I wanted to know how far apart they are on one machine with the settings most people never touch.
Short version: on a 4 dedicated vCPU machine, Redis 8.10.3 reached 8–9% more requests per second than Valkey 9.1.3, with default settings and with io-threads 3. Setting io-threads 3 raised both by about 12%, more than the gap between them. With three I/O threads, Valkey’s first I/O thread was the bottleneck; Redis still had headroom on every thread.
Results
The number is the highest request rate the server reached while k6 kept raising it (it asked for up to 45,000 req/s). Each round ran three times per server, in the order Redis, Valkey, Valkey, Redis, Redis, Valkey. The table shows the median run. None of the 82.7 million requests across the 12 runs failed.
| Round | Redis 8.10.3 | Valkey 9.1.3 | Gap |
|---|---|---|---|
Default settings (io-threads 1) | 34,863 req/s | 32,227 req/s | Redis +8.2% |
io-threads 3 | 39,306 req/s | 36,079 req/s | Redis +8.9% |
Gain from io-threads 3 | +12.7% | +12.0% |
The three runs behind each median:
| Round | Redis | Valkey |
|---|---|---|
io-threads 1 | 31,916 / 36,399 / 34,863 | 33,186 / 32,227 / 30,333 |
io-threads 3 | 39,713 / 38,988 / 39,306 | 36,614 / 32,692 / 36,079 |
With default settings the runs overlap: Valkey’s best run (33,186) beat Redis’s worst (31,916), and Redis came out ahead in two of the three pairs. With io-threads 3 every Redis run was above every Valkey run.
Memory after loading the same million products, from INFO memory:
| Redis | Valkey | |
|---|---|---|
used_memory | 236.0–236.1 MiB | 238.7–239.3 MiB |
These numbers hold for this workload on one 4 vCPU machine, with no cluster, no replicas and default persistence (RDB snapshots, no AOF). They say nothing about cluster mode, replication, heavy write persistence or bigger machines.
The setup
Four DigitalOcean droplets in lon1 on one private network:
| Machine | Size | Runs |
|---|---|---|
| kv-db | c-4: 4 dedicated vCPU, 8 GB | Redis or Valkey in Docker, one at a time, plus redis_exporter |
| kv-app | c-16 | the Go app |
| kv-load | c-16 | k6 |
| kv-mon | s-2vcpu-4gb | Prometheus and Grafana |
The app and load machines are oversized on purpose, so the only thing that runs out is the server. The server runs with --network host and only three flags: the private address, the port and --io-threads.
docker run -d --name kv --network host redis:8.10.3-trixie \
redis-server --bind $KV_DB_IP --port 6379 --protected-mode no --io-threads 1
docker run -d --name kv --network host valkey/valkey:9.1.3-trixie \
valkey-server --bind $KV_DB_IP --port 6379 --protected-mode no --io-threads 1
Valkey answers INFO server with redis_version:7.2.4 next to valkey_version:9.1.3; the second field is its real version.
The Redis 8 image loads its default modules (Search, JSON, Bloom, TimeSeries and vector sets). Nothing in the test calls them.
The app and the traffic
A small Go service with go-redis v9.23.0 and a pool of 200 connections. One k6 iteration is one user request, and the app turns it into two or three round trips to the server:
| Share | HTTP request | Commands |
|---|---|---|
| 60% | GET /products/{id} | HGETALL product:{id}, INCR views:{id}, ZINCRBY trending 1 {id} |
| 20% | GET /trending | ZREVRANGE trending 0 9 WITHSCORES, then one pipeline of 10 HMGET |
| 20% | POST /cart/{user} | HINCRBY cart:{user} {product} 1, EXPIRE cart:{user} 1800 |
Before each run the app loads 1,000,000 product hashes with 8 fields each, 1,000 per pipeline:
pipe.HSet(ctx, fmt.Sprintf("product:%d", j),
"name", fmt.Sprintf("Product %d", j),
"price", fmt.Sprintf("%d.%02d", 5+j%495, j%100),
"category", cats[j%len(cats)],
"stock", strconv.Itoa(j%250),
"rating", fmt.Sprintf("%d.%d", 1+j%5, j%10),
"brand", fmt.Sprintf("Brand %d", j%300),
"sku", fmt.Sprintf("SKU-%08d", j),
"description", "A product used in the Redis vs Valkey load test.")
k6 uses a ramping-arrival-rate executor, so it sends a fixed number of requests per second whatever the latency is. It starts at 10,000 req/s and adds 5,000 every 35 seconds (a 5 s ramp, then a 30 s hold) up to 45,000:
const stages = [];
for (let i = 0; i < STEPS; i++) {
stages.push({ target: START + i * STEP, duration: '5s' });
stages.push({ target: START + i * STEP, duration: HOLD });
}
export const options = {
scenarios: { shop: { executor: 'ramping-arrival-rate', startRate: START, timeUnit: '1s',
preAllocatedVUs: 3000, maxVUs: 10000, stages } },
discardResponseBodies: true,
// no `url` tag: every /products/{id} would become its own Prometheus series
systemTags: ['status', 'method', 'name', 'scenario', 'expected_response', 'check'],
};
Each step is measured over its last 25 seconds, from k6’s metrics in Prometheus.
Where latency broke
Peak throughput hides when each server stopped being fast. Here is p95 latency per step in the median runs:
| k6 asked for | Redis, io-threads 1 | Valkey, io-threads 1 | Redis, io-threads 3 | Valkey, io-threads 3 |
|---|---|---|---|---|
| 10,000 | 0.7 ms | 0.7 ms | 0.8 ms | 0.7 ms |
| 15,000 | 0.8 ms | 0.8 ms | 0.9 ms | 0.7 ms |
| 20,000 | 1.7 ms | 117.2 ms | 1.3 ms | 1.0 ms |
| 25,000 | 14.1 ms | 67.1 ms | 2.2 ms | 1.3 ms |
| 30,000 | 119.4 ms | 304.6 ms | 8.1 ms | 137.9 ms |
| 35,000 | 274.0 ms | 283.1 ms | 152.2 ms | 239.6 ms |
Below 20,000 req/s the two are the same within a tenth of a millisecond. The difference that matters for most deployments is how much traffic each one takes before p95 jumps from about a millisecond to over a hundred. With io-threads 3, Redis stayed under 10 ms up to 30,000 req/s in this run and Valkey up to 25,000. These are single runs, so read the exact step where each one broke as approximate. results/summary.json in the repository has every step of all 12 runs.
Threads: who ran out of what
pidstat -t on the server during each run shows CPU per thread. At the peak step:
| Round | Redis | Valkey |
|---|---|---|
io-threads 1 | main thread 93–96% | main thread 97–98% |
io-threads 3 | main 67–77%, I/O threads 69–80% | main 91–96%, first I/O thread 99.6–99.9% |
pidstat -t -u -h -p $(pgrep -x redis-server | head -1) 1
With one thread, both servers execute every command and do all the socket reads and writes on the main thread. One core sat near 100% and the other three were mostly idle.
io-threads moves reading requests and writing replies to extra threads; commands still run on the main thread. --io-threads 3 means the main thread plus two I/O threads. For a four-core machine, Redis’s sample config suggests 3 and Valkey’s suggests 2 or 3. With three, Valkey’s first I/O thread hit its ceiling, so Valkey had used up what this setting could give it. Redis did not max out any thread, so something outside Redis stopped it near 39,000 req/s. A Redis rerun with the app’s pool raised from 200 to 600 connections peaked at about the same rate. Its real limit on this machine is higher than the table shows.
The license
The bigger difference between them is legal, not speed:
- Redis 7.4 moved to RSALv2 or SSPLv1 in March 2024. Redis 8 added AGPLv3 as a third option.
- Valkey continued from Redis 7.2.4 under the BSD 3-clause license, with AWS, Google, Oracle, Ericsson and Snap backing it from the start.
Teams whose legal policy rules out AGPL and source-available code are left with Valkey. Where both are allowed, this test puts the trade at about 8–9% more throughput for Redis, with memory within about 1% of each other.
Run it yourself
Everything is in huseyinbabal/db-wars, redis-vs-valkey/: the app, the k6 script, the droplet setup scripts, the Grafana dashboard and the per-step results.
cd redis-vs-valkey/setup
./up.sh && ./provision.sh # 4 droplets, tag dbwars5
cd ..
./batch.sh # 2 rounds x 3 runs per server, alternating order
python3 analyze.py # results/summary.json
./setup/down.sh # delete the droplets
A pull request that changes the app or ci/compose.yml (for example --io-threads 3) gets a short version of the same test on a GitHub runner, with the numbers in a PR comment. All comparisons in the series are on db-wars.huseyinbabal.dev.