Valkey 9.1 performance work is the reason the 9.1 line exists. The 9.1.0 release (2026-05-19) reworked the I/O threading queues, doubled the embedded-string threshold, turned the processor clock on by default, embedded sorted-set elements in the skiplist, and cut the hot path for stream range reads. The two follow-on releases, 9.1.1 (2026-07-21) and 9.1.2 (2026-09-01), are security releases, but each also carries an I/O-thread patch that changes what you will see under load.
This post covers what actually changed inside the engine, which configuration parameters and INFO fields now matter, and how we measured it on a build of 9.1.2 from source. Every vendor figure is labelled as vendor-reported; our own numbers are labelled as sandbox-illustrative and are not a benchmark finding.
If you are running Valkey 8.x or 9.0 in production, the Valkey 9.1 performance summary is short: 9.1 changes memory accounting for small strings and sorted sets, changes how I/O threads schedule work, and adds telemetry that lets you finally see whether your I/O threads are doing anything. All three affect capacity planning, so re-baseline before you upgrade rather than after.
Valkey 9.1 release line status as of September 2026
Three releases exist on the 9.1 line. The Valkey 9.1 performance changes discussed here landed in 9.1.0; the point releases are security-driven but not performance-neutral.
| Release | Date | Upgrade urgency | Valkey 9.1 performance and security content |
|---|---|---|---|
| 9.1.0 | 2026-05-19 | LOW (first stable) | All 9.1 engine work: I/O threading redesign, 128-byte embedded strings, processor clock default, skiplist embedding, XRANGE hot path, COMMAND caching, database-level ACLs, Lua as a module, JSON logging, thread-usage metrics, MSETEX, HGETDEL, CLUSTERSCAN |
| 9.1.1 | 2026-07-21 | SECURITY | CVE-2026-56684 (TLS use-after-free via CLIENT KILL), CVE-2026-63639 (stream RDB shared NACK). Also offloads object deallocation to I/O threads (#3938) and fixes an I/O-thread reply-buffer race (#4060) |
| 9.1.2 | 2026-09-01 | SECURITY | GHSA-jcj7-v34w-v9vv (RDMA use-after-free, USE_RDMA builds only), GHSA-fq2f-crmw-q97r (Lua debugger state use-after-free). Also an I/O-thread optimisation for small payloads (#4401) |
Support dates matter for the upgrade stance later in this post. Per the Valkey release policy, 9.1 receives maintenance releases until 2029-05-19 and security fixes until 2031-05-19, because it is the last minor of the 9.x major. 9.0 loses both maintenance and security support on 2028-10-21. 8.1 is maintained until 2028-03-31 with security support to 2030-03-31.
Lab setup behind every Valkey 9.1 performance output in this post
Methodology first, results second. Everything below was captured on a single disposable Linux x86_64 sandbox with 2 vCPUs and 7 GB RAM. Valkey 9.1.2 was built from the 9.1.2 tag with MALLOC=libc because jemalloc was not available in the sandbox toolchain, which means the MEMORY USAGE figures are libc-allocator figures and will differ from a production jemalloc build. The benchmark client ran on the same two cores as the server. That co-location is exactly the situation the Valkey I/O-threading documentation tells you to avoid, and we kept it deliberately because it demonstrates the failure mode you will hit if you enable I/O threads on an under-provisioned host.
# Valkey 9.1 performance lab: build 9.1.2 from the release tag git clone --depth 1 --branch 9.1.2 https://github.com/valkey-io/valkey.git valkey-9.1.2 cd valkey-9.1.2 make -j2 MALLOC=libc src/valkey-server --version # Valkey server v=9.1.2 sha=7f1dffed:0 malloc=libc bits=64 build=d1c33521189c8709 src/valkey-server --port 6399 --daemonize yes --save "" --appendonly no \ --io-threads 1 --log-format json --logfile /var/tmp/vk1.log src/valkey-cli -p 6399 INFO server | grep -E "valkey_version|monotonic_clock|io_threads_active" # valkey_version:9.1.2 # monotonic_clock:X86 TSC @ 2100.04 ticks/us # io_threads_active:0
Nothing in this Valkey 9.1 performance post is a Redis-versus-Valkey comparison. No vendor-neutral head-to-head benchmark between Redis 8.x and Valkey 9.1 exists, and the numbers in circulation from either side are not comparable because payload size, pipeline depth, thread counts and TLS state all differ. We treat that absence as a finding and do not fill it with guesses.
The Valkey 9.1 performance headline: a redesigned I/O threading model
Valkey’s I/O threads already differed from the classic socket-offload design before 9.1: since 8.0 the I/O threads own protocol parsing, epoll_wait polling and, when enabled, memory prefetching of the hash-table entries the next batch of commands will touch. The main thread still executes every command.
What 9.1 changed is the hand-off between the two sides. The job queues between the main thread and the I/O threads were replaced with a lock-free queue implementation in PR #3324, which the release notes credit with an 8 to 17 percent throughput gain (vendor-reported, measured by the Valkey maintainers, not by us). The Valkey project’s own headline Valkey 9.1 performance number is 2.1 million requests per second on a single server with 512-byte payloads, 9 I/O threads and a pipeline depth of 10, again vendor-reported.
For Valkey 9.1 performance the mechanism matters more than the percentage. The main thread and the I/O threads exchange clients through job queues: the main thread hands a client to an I/O thread for reading and parsing, gets it back with a parsed command, executes it, and hands the client off again for the write. Any synchronisation on those queues is paid once per client per event-loop iteration, so at hundreds of thousands of requests per second even a short critical section becomes the main thread’s dominant non-execution cost.
The 9.1 lock-free queues bound the dispatch cost so it does not grow with the number of I/O threads, which is why the gain the maintainers report widens with thread count. The practical result is that the plateau 8.x users saw beyond four or five I/O threads moves out, provided the host actually has the cores.
Two follow-on patches change the Valkey 9.1 performance picture further. In 9.1.1, PR #3938 moves object deallocation for client-driven deletes onto the I/O threads, which shows up as a new io_threaded_freed_objects counter in INFO stats. In 9.1.2, PR #4401 optimises the I/O-thread path for small payloads, which is the regime most session-cache and counter workloads live in. Neither patch has a published percentage attached and we do not invent one.
Configuration parameters that control the threading model
All three parameters below are live in 9.1.2. io-threads is runtime-modifiable in Valkey, unlike the Redis lineage where it is a startup-only setting, and it is the parameter you will change most often while sizing.
| Parameter | Default | Range / unit | Change requires | Valkey 9.1 performance notes |
|---|---|---|---|---|
io-threads | 1 | 1 to build-time max, count | Reload (CONFIG SET) | 1 means single-threaded. io-threads-do-reads is deprecated and ignored. Leave at least one core for the main thread and one spare. |
prefetch-batch-max-size | 16 | 0 to 128, commands | Reload | 0 disables hash-table prefetching. With I/O threads, keys from multiple clients are batched together; without them only a single client’s pipeline is batched. |
server-cpulist | unset | taskset syntax | Restart | Pins main and I/O threads. Combine with bio-cpulist and bgsave-cpulist so the fork child does not land on an I/O-thread core. |
# Valkey 9.1 performance baseline: valkey.conf excerpt for a 16-vCPU host dedicated to one shard io-threads 8 prefetch-batch-max-size 16 server-cpulist 0-8 bio-cpulist 9,10 bgsave-cpulist 11-15 aof-rewrite-cpulist 11-15
Telemetry that tells you whether I/O threads are working
Before 9.1 the honest answer to “are my I/O threads busy” was to read per-thread CPU from /proc. 9.1 adds cumulative active-time counters for the main thread (PR #2931) and for each I/O thread (PR #2463) in the INFO cpu section, and the INFO stats section already carried the per-path counters. This is the output from the sandbox after switching to two I/O threads at runtime and running a GET load:
# Valkey 9.1 performance telemetry after switching to two I/O threads src/valkey-cli -p 6399 CONFIG SET io-threads 2 # OK src/valkey-cli -p 6399 INFO cpu | grep active_time # used_active_time_main_thread:29.727949 # used_active_time_io_thread_1:5.758641 src/valkey-cli -p 6399 INFO stats | grep io_threaded # io_threaded_reads_processed:348751 # io_threaded_writes_processed:348698 # io_threaded_freed_objects:0 # io_threaded_accept_processed:0 # io_threaded_poll_processed:20187 # io_threaded_total_prefetch_batches:255173 # io_threaded_total_prefetch_entries:3112868
Read these Valkey 9.1 performance counters as ratios, not absolutes. Sample used_active_time_main_thread twice over a fixed interval and divide by wall time; if the main thread is above roughly 80 percent busy while used_active_time_io_thread_N is low, the bottleneck is command execution and more I/O threads will not help. If the I/O threads are saturated and the main thread is not, add threads. io_threaded_total_prefetch_entries divided by io_threaded_total_prefetch_batches gives the effective batch size; in the sandbox run that is 12.2 against a configured maximum of 16, which means the pipeline depth of 10 was the limiting factor, as expected.
What the sandbox showed about Valkey 9.1 performance, and why it is not a benchmark
With io-threads 1, the 2-vCPU sandbox sustained the following on valkey-benchmark -t get,set -d 512 -P 10 -c 50 --threads 2 --warmup 3 --duration 10 -q. The --warmup and --duration flags are new in 9.1 (PR #2581) and are the correct way to run this tool now; request-count runs on a cold server measure allocator warm-up as much as the engine.
# Valkey 9.1 performance, io-threads 1, 2-vCPU sandbox, illustrative only SET: 698092.00 requests per second, p50=0.631 msec GET: 739773.00 requests per second, p50=0.559 msec
With io-threads 2 on the same host, GET throughput dropped to 511,142 requests per second with p50 0.807 ms and p99 4.327 ms. That is not a regression in Valkey 9.1; it is the benchmark client, the main thread and the I/O thread fighting over two cores. We include it because it is the single most common mistake we see in Valkey and Redis sizing reviews: I/O threads enabled on a 2- or 4-vCPU instance where the extra thread has nowhere to run. The 9.1 counters above make that mistake visible within a minute, which is the real operational gain.
These Valkey 9.1 performance figures are sandbox-illustrative: single run, libc allocator, co-located client. Do not cite these numbers. Run the same command on your own hardware with the client on a separate host.
Valkey 9.1 performance and memory: embedded strings up to 128 bytes
Valkey 9.0 already embedded the key, the expire field and small string values into a single allocation with the object header, removing a pointer dereference on lookup and an allocator header per key. 9.1 raises the embedded-string limit from 64 to 128 bytes (PR #3397). The Valkey blog attributes up to 30 percent higher GET throughput and up to 20 percent lower memory for strings under 128 bytes to this change (vendor-reported). The throughput part is cache behaviour: a key lookup for an embedded value touches one cache line group instead of chasing a pointer to a separate SDS allocation.
The 128 figure that anchors the Valkey 9.1 performance claims is not a value-length limit. In src/object.c, shouldEmbedStringObject() adds up the object header minus its pointer slot, the key’s SDS allocation, eight bytes for the expiry if one is set, and the value’s SDS allocation, and embeds only when that sum is at or under 128 bytes, which the source comment describes as two cache lines. So the value budget shrinks by one byte for every byte of key name and by eight bytes when the key has a TTL. We probed the boundary empirically on the 9.1.2 build:
# Valkey 9.1 performance probe: find the embedded-string boundary
probe() {
key=$1; ttl=$2
for n in $(seq 40 130); do
v=$(head -c $n /dev/zero | tr '\0' a)
if [ -n "$ttl" ]; then src/valkey-cli -p 6399 SET "$key" "$v" EX 3600 >/dev/null
else src/valkey-cli -p 6399 SET "$key" "$v" >/dev/null; fi
if [ "$(src/valkey-cli -p 6399 OBJECT ENCODING "$key")" = raw ]; then
echo "key=$key (${#key} bytes) ttl=${ttl:-no}: last embstr value len=$((n-1)), first raw=$n"; break
fi
done
}
probe kk; probe kk 1
probe session:user:0123456789abcdef; probe session:user:0123456789abcdef 1
probe "$(head -c 64 /dev/zero | tr '\0' K)" 1
# key=kk (2 bytes) ttl=no: last embstr value len=111, first raw=112
# key=kk (2 bytes) ttl=1: last embstr value len=103, first raw=104
# key=session:user:0123456789abcdef (29 bytes) ttl=no: last embstr value len=84, first raw=85
# key=session:user:0123456789abcdef (29 bytes) ttl=1: last embstr value len=76, first raw=77
# key=KKKK...K (64 bytes) ttl=1: last embstr value len=40, first raw=41
src/valkey-cli -p 6399 MEMORY USAGE kk # embstr, 111-byte value
# (integer) 136
src/valkey-cli -p 6399 MEMORY USAGE session:user:0123456789abcdef # raw, 85-byte value
# (integer) 160
That table is the capacity-planning input. A 29-byte session key with a TTL embeds values up to 76 bytes; a 64-byte key with a TTL embeds only up to 40. The MEMORY USAGE figures are libc-allocator figures and jemalloc size classes will shift them, but the shape holds: crossing the budget costs a second allocation and, on jemalloc, a jump to the next size class.
If your value-size distribution has mass between the 8.x threshold and the new budget, which is typical for compact JSON session blobs and serialised tokens, expect measurable savings from 8.x to 9.1. If your values are mostly above 128 bytes, or your key names are long, this change does little for you. Check before you plan around it:
# Valkey 9.1 performance planning: sample value lengths on a replica, never on the primary of a large keyspace
src/valkey-cli -p 6399 --scan --pattern 'sess:*' --count 1000 | head -50000 \
| xargs -n 500 sh -c 'for k in "$@"; do src/valkey-cli -p 6399 STRLEN "$k"; done' _ \
| sort -n | awk '{a[NR]=$1} END {print "p50", a[int(NR*0.5)], "p90", a[int(NR*0.9)], "p99", a[int(NR*0.99)]}'
Two related 9.1 changes stack on top. PR #2516 removed an internal server object pointer from small string objects, and PR #3306 improved client output buffer accounting via copy avoidance, so that large replies are referenced rather than copied into the output buffer when enough I/O threads are present.
The copy-avoidance reply path is gated by a hidden configuration, min-io-threads-avoid-copy-reply, whose default in the 9.1 source (src/config.c, checked in src/networking.c and src/memory_prefetch.c) is 7; our reading is that below that thread count the maintainers judged the copy cheaper than the bookkeeping. This is why the vendor’s 2.1 million requests per second figure was produced at 9 I/O threads and why a 4-thread deployment will not reproduce it regardless of hardware.
Valkey 9.1 performance from the processor clock, now on by default
Every command execution in Valkey reads the monotonic clock several times: for the slow log, for latency tracking, for expiry checks and for INFO counters. Reading clock_gettime through the vDSO is cheap, but reading the TSC via rdtsc on x86_64 or cntvct_el0 on aarch64 is cheaper still, which is why the clock source belongs in any Valkey 9.1 performance review. Valkey 9.1 makes the processor clock the compile-time default (PR #3103); the Valkey blog attributes up to 3 percent overall gain on GET and SET to it (vendor-reported). You can see which clock a running server chose in INFO server, and the sandbox reported monotonic_clock:X86 TSC @ 2100.04 ticks/us.
The operational caveat for this Valkey 9.1 performance gain is on the hardware side. The TSC path requires the constant_tsc CPU flag on x86_64 and a stable clock rate; the initialisation code falls back to the POSIX clock if it cannot calibrate, and logs the fallback at startup. On hypervisors that expose an unreliable TSC the fallback happens automatically, but if you run Valkey on hosts where TSC has been a problem for other software, you can build with CFLAGS="-DNO_PROCESSOR_CLOCK" to force the POSIX path. There is no runtime configuration for this; it is a build decision.
# Valkey 9.1 performance clock-source check grep -o -w constant_tsc /proc/cpuinfo | head -1 # constant_tsc src/valkey-cli -p 6399 INFO server | grep monotonic_clock # monotonic_clock:X86 TSC @ 2100.04 ticks/us # A POSIX fallback reports instead: # monotonic_clock:POSIX clock_gettime
Valkey 9.1 performance in sorted sets, streams, WATCH and SIMD
Four Valkey 9.1 performance changes are individually modest and collectively significant for specific workloads.
Sorted sets embed the element string inside the skiplist node (PR #2508) and embed the skiplist header inside the zset structure (PR #2867). The Valkey blog puts the memory saving at up to 10 percent for sorted sets (vendor-reported). The query-side effect is the same cache-locality argument as embedded strings: a ZRANGEBYSCORE walk no longer dereferences a separate SDS per node. Leaderboards, rate-limiter windows and delayed-job queues built on ZADD with timestamps as scores are the beneficiaries.
XRANGE and XREVRANGE gained a hot-path optimisation for the stream range walk (PR #3002); the Valkey blog claims up to 30 percent faster range reads (vendor-reported). Consumer-group readers using XREADGROUP are not on this path, so measure them separately if streams are your primary workload.
WATCH duplicate-key detection moved from an O(N) scan of the client’s watched-key list to an O(1) per-database hash-table lookup (PR #3360). This only matters if you have clients that WATCH hundreds of keys before a MULTI, which is more common in optimistic-locking libraries than their authors admit.
On ARM, pvFind() in the vector-set code path received a NEON SIMD implementation (PR #3033) with a claimed 2 to 3 times speed-up for that function (vendor-reported). Valkey 9.0 had already added SIMD paths for BITCOUNT and HyperLogLog. Graviton and Ampere deployments get this without configuration.
Valkey 9.1 performance under rehash, and the replica-side changes
The hash-table side of the Valkey 9.1 performance work is aimed at tail latency rather than throughput. PR #3481 releases pages incrementally during rehash instead of in one large free, which removes a class of latency spikes when a large table finishes a resize. PRs #3073, #3144 and #3175 tighten rehashing, pause auto-shrink during bulk deletes so that a mass expiry does not trigger a shrink-then-grow cycle, and fix the table swap during shrink. If you have ever seen latency-monitor events attributed to expire-cycle or eviction-cycle coinciding with dictionary resizes, these are the patches to test against.
# Valkey 9.1 performance tail-latency check src/valkey-cli -p 6399 CONFIG SET latency-monitor-threshold 5 src/valkey-cli -p 6399 LATENCY LATEST # (empty array) on the idle sandbox; nothing crossed 5 ms # On a busy 8.x primary after upgrade, compare LATENCY HISTORY expire-cycle # before and after; the 9.1 incremental release should shorten the tail.
On the replication side, two Valkey 9.1 performance changes reduce the cost of a full sync. Writable replicas now free keys asynchronously (PR #2849), and a replica that completes a disk-based full sync can reuse the received RDB file as the AOF preamble instead of rewriting it (PR #1901). The second one is easy to overlook and is worth a diagram, because it removes an entire fork-and-rewrite cycle from replica bootstrap when appendonly yes is set, which is exactly when the replica is least able to afford the extra I/O and copy-on-write memory.
The new rdb_transmitted replica state (PR #2833) and the exposure of dual-channel replication buffers in MEMORY STATS (PR #2924) give you the observability to confirm the behaviour in your own topology.
New commands with a Valkey 9.1 performance angle
MSETEX (PR #3121) sets multiple keys with a single shared expiry and honours NX. Before 9.1 the equivalent was a MULTI block of SET ... EX commands or a Lua script, both of which cost a round trip per key or a script invocation. On the sandbox:
# Valkey 9.1 performance commands: MSETEX src/valkey-cli -p 6399 MSETEX 2 sess:a "tok-a" sess:b "tok-b" EX 30 # (integer) 1 src/valkey-cli -p 6399 TTL sess:a # (integer) 30 src/valkey-cli -p 6399 MSETEX 1 sess:a "tok-x" NX EX 30 # (integer) 0 -- NX refused because sess:a exists src/valkey-cli -p 6399 GET sess:a # "tok-a"
HGETDEL reads and deletes hash fields atomically, which collapses the read-then-delete pattern used by job queues and one-time tokens into a single command. It pairs with the hash-field expiration family that arrived in 9.0, and 9.1 extends HSETEX with NX and XX flags (PR #2668).
# Valkey 9.1 performance commands: HGETDEL and HSETEX
src/valkey-cli -p 6399 HSET job:1 payload '{"x":1}' state queued
# (integer) 2
src/valkey-cli -p 6399 HGETDEL job:1 FIELDS 1 payload
# 1) "{\"x\":1}"
src/valkey-cli -p 6399 HGETALL job:1
# 1) "state"
# 2) "queued"
src/valkey-cli -p 6399 HSETEX job:1 EX 60 FIELDS 1 lock owner1
# (integer) 1
src/valkey-cli -p 6399 HTTL job:1 FIELDS 1 lock
# 1) (integer) 60
CLUSTERSCAN (PR #2934) scans keys across all shards of a cluster from one connection, and the 9.1.0-rc2 refinement (PR #3380) restricts the scan to the specific slots implied by a MATCH pattern with a hash tag. Together with the configurable hash-seed (PR #2608), which makes SCAN cursors consistent across nodes, this is the first time a cluster-wide keyspace audit does not require a client-side fan-out. Note that hash-seed is immutable at runtime and must be identical on every node; set it in valkey.conf before the node joins.
COMMAND responses are now cached (PR #2839). This is not a data-path change, but every client library that calls COMMAND or COMMAND DOCS on connect was paying for a large reply to be built per connection, and under connection storms that showed up as main-thread time. Connection-heavy PHP and serverless deployments benefit most.
Security changes that touch the data path
Database-level ACLs (PR #2309) add a db= selector so a user can be confined to numbered databases, including in cluster mode now that 9.0 made numbered databases cluster-aware. The check is a bitmap test per command and has no measurable cost, but the operational win is that multi-tenant deployments no longer need one process per tenant to get isolation:
# Database-level ACL on the Valkey 9.1 performance lab instance
src/valkey-cli -p 6399 ACL SETUSER app_ro on '>${VALKEY_APP_RO_PASSWORD}' '+@read' '~*' 'db=0,1'
# OK
src/valkey-cli -p 6399 ACL LIST
# 1) "user app_ro on #<sha256> ~* resetchannels db=0,1 -@all +@read"
# 2) "user default on nopass ~* &* +@all"
Lua scripting is now a module (PR #2858), statically linked by default since 9.1.0-rc2 (PR #3392), and reported in a new INFO section. The 9.1.2 security fix GHSA-fq2f-crmw-q97r lives in this code, so if your workload does not use EVAL or FUNCTION, building without the Lua engine removes both the attack surface and the memory it holds:
# Lua engine memory on the Valkey 9.1 performance lab instance src/valkey-cli -p 6399 INFO everything | grep -A4 "# Scripting Engines" # # Scripting Engines # engines_count:1 # engines_total_used_memory:69632 # engines_total_memory_overhead:56 # engine_0:name=LUA,module=lua,abi_version=4,used_memory=69632,memory_overhead=56
TLS certificates now reload automatically in the background (PR #3020), INFO tls reports tls_server_cert_expires_in_seconds and its client and CA counterparts (PR #2913), and mTLS can authenticate on a SAN URI (PR #2999). Structured logging via log-format json (PR #1791) makes the reload events parseable; the sandbox log above was produced with it.
Valkey 9.1 performance upgrade stance: adopt 9.1.2 now
Adopt 9.1.2 now on any 9.0.x or 9.1.x estate. The two 9.1.2 advisories are remote-code-execution class in the Lua case and the fix set is small; the corresponding 9.0.6, 8.1.10 and 8.0.11 releases shipped the same day for estates that cannot take a minor upgrade yet. For 8.x estates moving to 9.1, treat it as a capacity re-baseline rather than a drop-in, for three reasons: memory accounting for strings and sorted sets changes, I/O-thread scheduling changes, and the atomic slot migration path from 9.0 (now the default in valkey-cli --cluster rebalance via --cluster-use-atomic-slot-migration, PR #2755) changes how resharding load looks.
Two behaviour changes deserve a line in your change record. Strict TLS certificate validation at config load was reverted in 9.1.0-rc2 and deferred to the next major (PR #3572), so a configuration that loads on 9.1 may fail on 10.0. And cluster-config-save-behavior (PRs #1032 and #3372) is new. The default, sync, keeps the traditional behaviour where a failed nodes.conf save exits the process; best-effort logs a warning and keeps serving, retrying on the next configuration change, which lets a node survive a transient disk fault at the cost of possibly loading stale cluster state if it restarts before the disk recovers. Choose best-effort only where you already alert on the warning line.
The configuration deltas we apply on a 9.1 primary, stated in the form we use in change records:
| Parameter | Current (8.x typical) | Proposed | Unit | Reload or restart | Valkey 9.1 performance metric that justifies it |
|---|---|---|---|---|---|
io-threads | 1 | vCPUs minus 2, capped at 8 to start | threads | Reload | used_active_time_main_thread vs used_active_time_io_thread_N ratio over a 60 s window |
prefetch-batch-max-size | 16 | 16 (raise to 32 only with pipelining clients) | commands | Reload | io_threaded_total_prefetch_entries / io_threaded_total_prefetch_batches |
log-format | legacy | json | enum | Reload | Log pipeline parse errors |
latency-monitor-threshold | 0 | 10 | ms | Reload | LATENCY HISTORY expire-cycle before and after |
cluster-config-save-behavior | n/a | sync (best-effort only with alerting in place) | enum | Reload | nodes.conf save-failure warning in logs |
Rollback path: io-threads, prefetch-batch-max-size and the other parameters in the table are runtime-reversible with CONFIG SET. A binary downgrade is a different matter. Valkey does not guarantee that an RDB written by a newer release loads on an older one, and 9.x data-path features such as hash-field expiration have no representation in 8.x, so the only dependable rollback from a 9.1 primary is a replica still on the previous binary that has not yet been promoted. Keep one out of the write path until the 9.1 primary has run through a full peak cycle, and verify that it is still in sync with INFO replication before you rely on it.
Where Valkey 9.1 performance changes do not help
Being explicit about where the Valkey 9.1 performance gains stop is what makes the rest credible. Single-connection, non-pipelined workloads will not see the I/O-threading gains because there is nothing to batch. Values above 128 bytes get no benefit from the embedded-string change. Workloads dominated by Lua scripts inherit none of the command-path improvements inside the script body. Hosts with fewer than four vCPUs should leave io-threads at 1, as the sandbox run demonstrates. And the vendor-reported figures quoted throughout were produced on hardware and with a client configuration that the Valkey blog describes but that we did not reproduce; treat them as an upper bound on what a well-shaped workload can expect, not as a forecast.
Test every Valkey 9.1 performance assumption on a replica or a staging shard that carries a replayed slice of your production command mix from INFO commandstats, keep a verified RDB and a tested restore procedure before any binary upgrade, and maintain a DR posture that does not depend on the version you are moving away from.
How MinervaDB can help
MinervaDB runs Valkey and Redis for clients across every major cloud and on bare metal, and the Valkey 9.1 performance upgrade cycle is exactly where sizing assumptions built on 7.x and 8.x break. Our Redis and Valkey support and consulting practice covers upgrade planning with a measured capacity baseline, I/O-thread sizing from the counters described in this post, cluster resharding with atomic slot migration, and 24×7 support with a 15-minute S1 response. If you want the sandbox procedure above run against your actual command mix and hardware before you commit, that is a one-week engagement.
References
Sources for the Valkey 9.1 performance claims above. Valkey project, Valkey 9.1 delivers improvements in security, performance, and more (2026-05-19). Valkey project, valkey-io/valkey releases, including the 9.1.0-rc1, 9.1.0-rc2, 9.1.0, 9.1.1 and 9.1.2 notes with the PR numbers cited above. Valkey project, Releases and versioning schema, for the support calendar. Valkey source tree at tag 9.1.2, files valkey.conf, src/config.c, src/monotonic.c, src/object.c and src/server.c, for parameter names, defaults and INFO field names.