Valkey 9 makes field lifetimes and slot transfers explicit
Valkey 9 ships hash-field expiration, atomic slot migration and numbered cluster databases. The final fixes show how conditional writes and migration visibility affect correctness.
A hash is a useful grouping until its fields need different lifetimes. Expiring its top-level key removes every field together, while splitting the fields into separate keys changes the application's representation. Valkey 9.0.0, released on October 21, adds field-level expiration so that a hash can retain its grouping while individual values expire. The release also makes slot transfer an atomic migration operation and allows numbered databases in cluster mode.
Those additions expand the data and operational model beyond the shared Redis 7.2.4 starting point. They also introduce new conditions which must hold around existing operations. A field update that fails its condition must leave the key unchanged. Data copied during migration must remain hidden until ownership transfers. The final release includes corrections to those cases as well as the new capabilities themselves.
I default to Valkey when Redis semantics are wanted, and this is the kind of independent development that makes the choice interesting. Compatibility remains useful, but a release also needs to describe where its behavior now extends beyond that baseline. Field lifetime and slot ownership are concrete examples.
Lifetime becomes part of the field operation
The HSETEX definition shipped with 9.0.0 combines field updates with expiration options. Relative expiration can use seconds or milliseconds, while absolute expiration uses a Unix timestamp. A keep-TTL option supports retaining an existing lifetime. The command also has field-existence conditions, making the intended write more specific than an unconditional hash update.
HSETEX session:42 EX 300 FIELDS 1 verification pending
This stores one field and supplies its lifetime in the same operation. The application need not first write the field and then issue a separate request to add expiration. That removes a gap in which the field would exist without the intended lifetime if the second request failed or never arrived.
For fields already present, HEXPIRE applies expiration to an explicit list. Its result contains a code for each requested field, preserving order. A missing field and an unmet condition are distinguishable from applying the expiration. Code consuming a multi-field result therefore needs to retain the association between each field and its status.
HEXPIRE session:42 60 FIELDS 1 verification
These are field lifetimes under one hash key. They do not change the top-level key into a collection of independently routed cluster keys. Grouping remains relevant to the application's operations, while expiration metadata now exists at a finer granularity. That distinction determines when the feature can replace a separate-key representation and when it cannot.
A failed condition must not create an empty object
The final HSETEX correction addresses the FXX condition on a nonexistent key. Previously, the implementation could create the hash object before checking whether the required fields existed. Returning a failed conditional result then left a database object behind despite declining the requested update.
The repair evaluates the condition before creating the hash. If the key is absent, FXX fails immediately because none of the required fields exists. FNX has the opposite relationship to that absence, since the missing key cannot already contain any of the requested fields. The test checks both the failed return value and that the top-level key remains absent.
This matters to more than the returned status. Applications can observe key existence independently of a field value, and an unsuccessful conditional operation should have the side effects its contract permits. Checking only the command's integer result would miss a change to the surrounding database state. The regression test records the stronger postcondition.
Slot transfer separates copying from ownership
The atomic migration implementation transfers slot data using a snapshot followed by replicated changes. The source drives an asynchronous job, while the destination receives data before the handoff completes. A final coordination step transfers responsibility for the slots. That makes the interval between physical arrival and logical ownership part of the server's model.
This is an important meaning of atomicity. It describes the ownership transition, rather than claiming that the network transfer takes no time. The destination needs incoming data so that it can take over, but ordinary readers must not infer that receiving a key is equivalent to owning it. The migration state controls that distinction.
The replica-visibility correction extends importing-state handling to replicas. It persists import information in the relevant RDB path so that a replica resynchronizing during an import does not expose unfinished data. A destination primary's visibility policy is insufficient if its replica can lose the state needed to apply the same policy.
That patch illustrates why replication correctness includes metadata beyond values. Copying every key's bytes can still produce the wrong observable database if the recipient does not know which keys are provisional imports. The information needed to interpret data must survive the synchronization path alongside the data itself.
Completed migration needs consistent accounting
The DBSIZE accounting repair addresses the transition out of importing state. It changes the bookkeeping so that removing a slot from the importing set determines whether its key count should enter the normal cumulative count. It also subtracts the corresponding count from importing-key accounting.
The tests move slots and compare counts on both primaries and replicas, then move data back. This checks a different property from successful reads after a transfer. A database can contain the expected keys while reporting an incorrect total if the transition updates accounting twice or leaves a provisional count behind.
Correct totals matter because administrative tools often compare data before and after resharding. If an import's temporary state leaks into stable counts, those tools can report apparent loss or duplication even when the stored values are available. The correction makes the bookkeeping follow the actual transition from importing to ordinary ownership.
Cancellation has a separate lifetime problem. Pull request 2721 fixes a race when aborting migration while the snapshot child remains active. It connects cleanup of the migration connection with stopping the child and makes the pipe handler recognize that its destination has closed. Aborting a job must account for work already running outside the immediate command handler.
I find those final fixes as informative as the headline feature. Migration is an operation with intermediate state, counters and background work. Each part needs a defined outcome on completion or cancellation, and the release's fixes identify several places where those outcomes meet.
The release also fixes a duplicate MOVED response when unblocking at failover. Redirecting a waiting command needs to finish its blocked state without arranging another response through reprocessing. The patch changes that unblocking path. For a client, one command's response has to remain identifiable even when cluster ownership changes while it waits. A correct destination address alone would not be enough if the server emitted the response twice and disturbed command-reply correspondence.
Modules and databases remain part of the contract
The module support requirement introduced during the candidates remains relevant. Modules must opt into atomic slot migration because keys can load before ownership transfers. The server also supplies lifecycle events so extensions can distinguish import and export outcomes. A successfully loaded extension does not automatically establish support for every new cluster operation.
Additional migration context flags identify import and export clients to modules and client inspection. That makes the role of an internal connection observable without guessing from its command traffic. Extensions can interpret work in the context which produced it, while operators can distinguish migration connections from ordinary application clients.
Numbered database support keeps the cluster's hashing algorithm unchanged. The same key name maps to the same slot across database numbers. Management remains concerned with cluster-wide ownership, while key enumeration in a slot uses the selected database. Tooling which previously assumed database zero must account for that larger set of namespaces.
The stable release therefore combines finer data lifetimes with a more explicit transfer lifecycle. An application adopting field expiration needs the per-field result semantics. An operator using atomic migration needs job outcomes and module support, together with correct visibility and accounting across replicas. Those requirements describe the practical behavior delivered by Valkey 9's new operations.