BuildNot yet confirmed elsewhere1 publisher3 min readPublished
Valkey 9.2's INCREX closes the crash gap that strands INCR/EXPIRE counters without a TTL
Valkey 9.2.0-rc1's new INCREX command left no counters without an expiry in one developer's 500-request crash test, against 11 for INCR then EXPIRE. Most of its speed gain comes from dropping one round trip, so the case for adopting it is correctness.
The Engineer · Build desk
What happened
- INCREX first appears in the 9.2.0-rc1 release candidate, and the previous stable build, 9.1.2, rejects it as an unknown command.
- The syntax takes an optional NX or XX flag, an EX, PX, EXAT or PXAT expiry, and a BYINT or BYFLOAT step that defaults to an increment of 1.
- Across three runs of 2,000 fresh keys, INCREX ran 1.6x to 2x faster than INCR and EXPIRE sent as two blocking calls.
- With the old pair pipelined into one network exchange, INCREX's lead shrank to between 9% and 21%.
- Twenty threads making 100 increments each on one shared key reached exactly 2,000, and the first caller's 30-second TTL stayed in place.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Teams that already pipeline the pair or wrap it in MULTI/EXEC will see little speed change, so their migration case rests on the closed crash window and the single call.
- constraint Code that carries EXPIRE's NX and XX habits over to INCREX will behave differently, because INCREX uses SET's convention for those flags, so flag use needs checking before rollout.
- constraint Fleet inventory cannot use INFO server to find release-candidate nodes, since a 9.2.0-rc1 server reports itself as plain 9.2.0.
The pattern INCREX replaces is three lines of application code, as the post's author writes them [9]:
```python val = r.incr(key) if val == 1: r.expire(key, 60) ```
Here is what happens when the process dies between the second and third lines. The increment has landed and the key holds 1. The next request gets 2, fails the check and skips EXPIRE. So does every request after it [20]. The author wrote that each such crash leaves "exactly one permanently orphaned key, because the increment already landed and nothing will ever set its expiry" [13].
INCREX [5] has no in-between state for a crash to land in. Either the process dies before the single command is sent and nothing happened, or it dies after the command completed and the key already holds its new value and its TTL [14].
The crash test gave each request a 2% chance of failing after the first command, and the observed rate came out at 2.2% [12][19]. That rate does not carry straight into production. Only the request whose INCR returns 1 runs EXPIRE, so only a crash on that request strands a key [20]. A counter that takes many increments per window creates its key once. The production orphan rate is therefore the crash rate on first increments, and each orphan persists [13].
The speed numbers came from Docker containers on one host, driven by redis-py 8.1.0 over localhost TCP [10]. The pipelined control run is the best thing in the post. It removes the second network wait, and what is left is the server-side difference [2]. That pipeline also drops the conditional and queues INCR and EXPIRE on every call, with transaction=False [11]. That is fine for a benchmark. In a limiter, an EXPIRE on every request sets the 60-second deadline again each time, so the window no longer starts at the first increment [21]. The post's own code makes the author's point that pipelining a conditional increment "is awkward enough that people skip it" [3].
I'd expect the blocking pattern's penalty to grow on a real network. It waits on the network twice per request, and a localhost round trip is about as short as round trips get [9][10].
Fixed-window quotas depend on later increments leaving the first TTL alone, which is the behaviour the 20-thread run showed [15]. The post does not show which flags those threads passed. The plain EX form's behaviour on an existing key needs its own check before a quota counter depends on it.
The build tested is a release candidate, the newest valkey/valkey tag on Docker Hub when the post was written [17]. The author wrote, "The round trip is not the part of this that matters." [18]
What to watch
- Whether the final 9.2.0 release keeps INCREX's rc1 grammar and its SET-style NX/XX semantics unchanged.
- A repeat of the benchmark across a real network, which would show how much the blocking pattern's penalty grows with round-trip time.
- Whether client libraries such as redis-py expose INCREX as a first-class method, which sets how easily existing limiter code can move to it.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence55
- Adoption
- Insufficient
- Hype gap0
- Incentives
- Insufficient
- Confidence50
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Across three runs each of 2,000 calls against 2,000 distinct keys, INCREX key EX 60 BYINT 1 was a repeatable 1.6x to 2x faster than the two-blocking-call pattern.
- [2]
When the old pattern was pipelined instead of sent as two blocking calls, the speed gap dropped to 9-21%; the author attributes the 2x figure to the removed round trip, not to INCREX being faster on the server side.
- [3]
"pipelining a conditional increment (if val == 1) is awkward enough that people skip it"
ReportedSupportedSource: Author of the dev.to post2 sources— create a free account to open themView cited source - [4]
Anyone already pipelining the INCR/EXPIRE pattern, or wrapping it in MULTI/EXEC, is not going to notice much of a speed difference, according to the author.
- [5]
Valkey 9.2.0-rc1 adds a command called INCREX that increments a key and sets its expiry in a single call.
- [6]
INCREX is not in Valkey 9.1.2, the previous stable release; calling it there returns ERR unknown command 'INCREX'.
- [7]
On 9.2.0-rc1, INFO server reports the version as plain 9.2.0 with no -rc1 suffix, so a candidate build cannot be told from a final one by querying the server.
- [8]
The INCREX grammar is INCREX key [NX|XX] [EX seconds|PX ms|EXAT ts|PXAT ts] [BYINT n|BYFLOAT n]; leaving out BYINT/BYFLOAT increments by 1.
- [9]
The pattern the author writes in application code is two separate blocking round trips: val = r.incr(key); if val == 1: r.expire(key, 60).
- [10]
The tests ran valkey/valkey:9.2.0-rc1 and valkey/valkey:9.1.2 as plain Docker containers on the same host, driven from a Python script using redis-py 8.1.0 over TCP on localhost.
- [11]
The pipelined version of the old pattern was pipe = r.pipeline(transaction=False); pipe.incr(key); pipe.expire(key, 60); pipe.execute(), with no val == 1 check.
- [12]
In a simulation of 500 requests, each with a 2% chance of crashing after the first command succeeds, the old pattern had 11 simulated crashes and 11 keys stuck with no TTL; INCREX had 11 simulated crashes and 0 keys stuck with no TTL.
- [13]
Under the old pattern, every simulated crash leaves "exactly one permanently orphaned key, because the increment already landed and nothing will ever set its expiry."
- [14]
Under INCREX, a crash either happens before the single command is sent, in which case nothing happened, or after it completed, in which case the key already has both its new value and its TTL.
- [15]
Twenty threads making 100 increments each against one shared key produced expected 2000, got 2000, ttl=30; the TTL set once by the first caller was left alone by the other 1,999.
- [16]
The NX/XX flags on INCREX do not mean what they mean on the standalone EXPIRE command (act only if the key does or does not already have a TTL); INCREX follows SET's convention.
- [17]
9.2.0-rc1 is a release candidate, not a final build, and was the newest tag on Docker Hub for the valkey/valkey image as of the test.
- [18]
"The round trip is not the part of this that matters."
- [19]
The observed crash rate in the simulation was 2.2%, close to the injected 2% chance.
- [20]
In the old pattern only the request whose INCR returns 1 calls EXPIRE, so a crash strands a key only on the request that creates it; later increments return values above 1 and skip EXPIRE, leaving the counter without a TTL permanently.
- [21]
The pipelined benchmark variant calls EXPIRE key 60 on every increment, so each call sets the expiry again and the window no longer starts at the first increment as it does in the conditional version.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toValkey 9.2's INCREX command removes the crash window between INCR and EXPIRE
1 article · October 7, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.