JusDB
Free DB Audit
NoSQL Databases

How Much RAM Does a Valkey Upgrade Save? Measuring Per-Key Memory from 7.2 to 9.1

See where Valkey 8.0, 8.1 and 9.1 cut per-key memory, what the vendor figures measure, and how to measure your own saving on a staging replica before you resize nodes.

JusDB Team
Published November 2, 2026
Updated November 10, 2026
20 min read

Valkey 8.0, 8.1 and 9.1 each cut the memory that Valkey spends on bookkeeping for every key, and the release posts quote numbers like "about 20 bytes per key" and "17 to 44% less string overhead". Then you upgrade a staging node, and used_memory falls by a different amount, or MEMORY USAGE returns a new number for a key that never changed. Both can be correct. The savings depend on your key and value sizes, your data types and whether you run cluster mode. Some of the metrics you would use to measure them also changed meaning between releases. This post lists where each release reduced per-key overhead, quotes the vendor figures as vendor figures, and gives a replica-based method to measure your own saving before you change a node count.

TL;DR
  • Per-key overhead fell in three steps: 8.0 (per-slot dictionaries in cluster mode and key embedding), 8.1 (a new hashtable that removes the dictionary entry), and 9.1 (smaller embedded strings, a 128-byte embedding budget and embedded sorted set members).
  • The published savings come from Valkey's own tests on small synthetic keys. Treat them as an upper reference, not a forecast for your data.
  • Measure with two replicas of the same primary, one on your current version and one on the target, and compare used_memory minus startup and backlog per key. used_memory_dataset and MEMORY USAGE changed their accounting across these releases.
  • Run the 9.1 test replica on 9.1.3 or later. Releases 9.1.0 to 9.1.2 cannot read some 9.0 listpack entries (PR #4819).
  • Resize only after production has restarted on the new version and you have re-measured, and keep the headroom policy you already use.

Scope and review date: November 2026. This post covers self-managed Valkey from 7.2 to 9.1.3, the current 9.1 patch released on 26 October 2026. Readers moving from Redis OSS 7.2 start from the same memory layout as Valkey 7.2. Vendor figures are quoted from valkey.io blog posts and GitHub release notes, and source behaviour was read from the tagged Valkey source on 10 November 2026. Commands were checked against the documentation and were not run in a lab, so rehearse them on staging. Anything from 9.2 is labelled as release-candidate behaviour.


Which Valkey releases reduced per-key memory?

Three releases changed how much memory Valkey spends on each key, apart from the key and value bytes themselves. Each one removed a different pointer or allocation, so the savings stack, but they apply to different key shapes.

  • 8.0, from the Valkey 8 memory efficiency post: cluster mode keeps one dictionary per hash slot, so each entry drops the two 8-byte pointers that linked keys within a slot. Separately, the key is embedded in the dictionary entry (PR #541).
  • 8.1, from A new hash table: the chained dict is replaced by a hashtable of 64-byte buckets that hold up to seven entries. The dictionary entry disappears, and the key and any expire time are embedded in the value object (PR #1186). Large hashes, sets and sorted sets use the same hashtable.
  • 9.1, from Reducing Memory Overhead in Valkey 9.1: embedded strings lose their redundant 8-byte pointer (PR #2516), the embedding budget rises from 64 to 128 bytes (PR #3397), and sorted sets in skiplist encoding embed each member in its node (PR #2508).

We found no per-key layout change in the 9.0 release notes, though 9.0 changed how MEMORY USAGE counts an embedded key (covered below).

Per-key overhead changes by upgrade step

Switch steps to see what each release removed; the savings rows are Valkey's own figures

7.2 to 8.0

AreaWhat changed
Main keyspace structureOne dictionary per slot in cluster mode; about 1 MB fixed cost per node
Where the key name livesEmbedded in the dictionary entry
Small strings (embstr)Unchanged: values up to 44 bytes
Keys with a TTLUnchanged: separate expires dictionary entry
Hash, set and zset elementsUnchanged
Vendor-reported saving16 B per key in cluster mode, plus about 8 B from key embedding
Vendor test16-byte values, 6,318,941 keys, single-shard cluster: 20.63% less used_memory

8.0 to 8.1

AreaWhat changed
Main keyspace structureNew hashtable: 64-byte buckets of up to seven entries, no dictionary entry
Where the key name livesEmbedded in the value object, with any expire time
Small strings (embstr)Key, expire and value share a 64-byte embedding budget
Keys with a TTLExpire stored inside the object
Hash, set and zset elementsHashtable encoding saves about 10 to 20 B per element
Vendor-reported savingAbout 20 B per key, about 30 B per key with a TTL
Vendor testOverhead graph across value sizes; Momento saw 22% on a 50M-member zset

9.0 to 9.1

AreaWhat changed
Main keyspace structureUnchanged
Where the key name livesUnchanged
Small strings (embstr)8-byte pointer removed; budget raised from 64 to 128 B
Keys with a TTLExpire still counts toward the 128-byte budget
Hash, set and zset elementsSkiplist zset members embedded: about 6 to 8.5 B per 10 to 40 B member
Vendor-reported saving17 to 44% of string overhead, average 26%; a few lengths regress
Vendor test5 million items per test across key and value sizes, 9.0 against 9.1

Overhead, not total: every figure here is a cut in bookkeeping bytes. A key with a 2 KB value spends most of its memory on the value, so the same byte saving is a much smaller share of its total.

How much did Valkey's own tests save?

The vendor tests are worth reading closely, because their setup explains most of the gap between their numbers and yours. All of them use small, uniform keys, which is where bookkeeping is the largest share of memory.

UpgradeTest shapeReported resultSource
7.2 to 8.0SET with 16-byte values, about 6.3 million keys, cluster mode693.64 MB to 550.56 MB, about 20.63%Valkey 8 memory efficiency post
8.0 to 8.1Overhead per key across value sizesAbout 20 B per key, about 30 B with a TTLA new hash table post; 8.1 GA post
8.0 to 8.1One sorted set, 50 million members4.83 GB to 3.77 GB, about 22%Momento's guest post
9.0 to 9.1Strings, 5 million items per test17 to 44% of overhead, average 26%9.1 memory efficiency post
9.0 to 9.1Sorted sets of 129 to 512 members9.9 to 10.8% less memoryPR #2508

The 8.0 test ran a 7.2 primary with two replicas on newer builds, which is also the method used later in this post. Its load was synthetic:

bash
# The synthetic load from the Valkey 8 memory efficiency post
src/valkey-benchmark -t set -n 10000000 -r 10000000 -d 16

Interpretation: every value is 16 bytes, which is close to the best case for per-key savings. It confirms your build behaves like the vendor's, but do not size a fleet from it.

Two numbers for the same 9.1 change

The 9.1 release post says "up to a 20% memory reduction for strings under 128 bytes". The 9.1 deep dive says 17 to 44% of string overhead. One is a share of total memory and the other a share of overhead, so always check what a percentage is a share of.

Why does your saving differ from the vendor figure?

Four things decide what an upgrade gives back on your data.

Allocator size classes

Valkey allocates through jemalloc, which rounds every request up to a fixed size class. The 9.0 source comment describes these as 16-byte steps up to 128 bytes. An 8-byte cut that moves an allocation from 88 bytes to 80 saves 16 bytes, because 88 rounds up to 96. One that moves it from 108 to 100 saves nothing, because both round up to 112. This is why the 9.1 post reports a range, and why "a handful of specific lengths see little or no benefit, and a few see a small regression" for sorted set members.

Which encoding a key uses

Each release changed a specific encoding. Small hashes, sets and sorted sets stored as listpack or intset do not use the hashtable for their elements, so they gain only the per-key saving. The 9.1 skiplist change applies only to sorted sets above the listpack limits.

bash
valkey-cli CONFIG GET hash-max-listpack-entries
valkey-cli CONFIG GET zset-max-listpack-entries
valkey-cli CONFIG GET zset-max-listpack-value

Interpretation: the 9.1.3 valkey.conf defaults are 512 hash entries, 128 sorted set entries and 64-byte values. A sorted set with 100 members stays a listpack and gets nothing from the skiplist embedding. One with 129 members becomes a skiplist and does.

The string embedding budget

In 7.2 and 8.0, a string value of up to 44 bytes is stored as embstr, in the same allocation as its object header. From 8.1 the key and any expire are embedded too, and the tagged source for 8.1 and 9.0 embeds the value only when header, key, expire and value together fit in 64 bytes. In 9.1.3 the budget is 128 bytes and the header is 8 bytes smaller. A short key with a TTL and a 40-byte value can therefore be raw on 9.0 and embstr on 9.1.

bash
valkey-cli OBJECT ENCODING session:8f3a
valkey-cli TTL session:8f3a
valkey-cli STRLEN session:8f3a

Interpretation: the three answers give the encoding, whether an expire is embedded, and the value length. Note that the OBJECT ENCODING page still says embstr is limited to 44 bytes. As of November 2026 that matches 7.2 and 8.0 only.

Cluster mode and TTLs

The 8.0 per-slot saving of 16 bytes applies only in cluster mode, so a standalone primary gets only the key-embedding part, about 8 of the 24 bytes. Keys with a TTL gain more in 8.1, about 30 bytes rather than 20, because their entry in the expires dictionary is replaced by a slot in a hashtable bucket. A cache where every key has a TTL and a short value is the shape that gains most.

Which memory metrics can you compare across versions?

Not all of them. Reporting changed alongside the layout, so a metric can move for accounting reasons alone.

  • used_memory is the total the allocator handed out. Its definition did not change, so it anchors any comparison.
  • used_memory_dataset is used_memory minus used_memory_overhead. In the 7.2 source, the keyspace overhead includes every dictionary entry and one object header per key. From 8.1 those structures no longer exist and the object holding the key counts as data. Across that boundary, part of the "dataset" change is a relabelling.
  • keys.bytes-per-key in MEMORY STATS is dataset.bytes divided by keys.count, so it inherits the same shift.
  • MEMORY USAGE in 7.2 and 8.0 adds the dictionary entry and the key to the value. From 8.1 it measures the object, which now contains the key, plus the value, and leaves out the key's slot in the hashtable bucket. On 8.1.0 to 8.1.3 it also missed the embedded key for most encodings, fixed in 8.1.4 and 9.0.0 (PR #2290).
bash
valkey-cli INFO memory | grep -E '^(used_memory|used_memory_startup|used_memory_overhead|used_memory_dataset|mem_replication_backlog|mem_fragmentation_ratio):'
valkey-cli MEMORY STATS

Interpretation: record all of these on both sides. Base the decision on used_memory minus used_memory_startup and mem_replication_backlog, divided by keys.count. Read mem_fragmentation_ratio separately: a freshly loaded node has little fragmentation whatever its version.

9.0 to 9.1 is the easy comparison

The 9.0 and 9.1 source compute MEMORY USAGE the same way, so per-key deltas between them are directly comparable. Across 7.2 or 8.0 and 9.1, use it to find which key shapes changed, not to total the saving.

How do you measure the saving on a staging replica?

Compare two replicas of the same primary: one on your current version and one on the target. Both get the same keys from the same full sync, both start with a fresh heap, and neither handles client writes, so the only difference left is the version. This is the method Valkey used for its 8.0 numbers.

Measuring per-key memory with two replicas

Follow the numbers: one primary feeds both replicas, and only the version differs

Side-by-side replica measurement of a Valkey upgrade Step 1: the existing primary, on the current version, sends a full sync to two new replicas: one on the current version as the baseline, and one on the target version, 9.1.3 or later. Step 2: once both links are up and key counts match, read INFO memory and MEMORY STATS on each replica and sample keys with MEMORY USAGE and OBJECT ENCODING. Step 3: compare used memory per key between the two replicas and use the difference for the sizing decision. Primary current version Replica A current version Replica B 9.1.3 or later Compare bytes per key 1 2 3 one full sync per replica same keys, same config, compare

Compare replica with replica: never compare the new replica with the long-running primary. The primary carries client buffers and fragmentation that have nothing to do with the version.

A newer replica of an older primary is the normal rolling-upgrade state. Each full sync forks the primary, so add the replicas in a quiet period or attach them to a staging copy of production.

bash
# Standalone or Sentinel: attach both test replicas to the primary
valkey-cli -h r-old REPLICAOF primary.internal 6379
valkey-cli -h r-new REPLICAOF primary.internal 6379

# Cluster mode: add each one as a replica of a chosen shard primary
valkey-cli --cluster add-node r-new:6379 primary.internal:6379 \
  --cluster-replica --cluster-primary-id <primary-node-id>

Interpretation: start both test nodes with the same cluster-enabled setting and memory-related configuration as production. In cluster mode, attach both to the same shard so they hold the same slots. replica-ignore-maxmemory defaults to yes, so the replicas will not evict keys on their own during the test.

bash
for h in r-old r-new; do
  valkey-cli -h "$h" INFO replication | grep -E '^(master_link_status|master_sync_in_progress):'
  valkey-cli -h "$h" INFO server | grep -E '^valkey_version:'
  valkey-cli -h "$h" DBSIZE
done

Interpretation: wait for master_link_status:up and master_sync_in_progress:0 on both. The key counts should match within the churn of a few seconds. If they differ by more, one replica is still loading or has broken its link.

bash
for h in r-old r-new; do
  mem=$(valkey-cli -h "$h" INFO memory)
  used=$(echo "$mem" | awk -F: '$1=="used_memory"{print $2+0}')
  start=$(echo "$mem" | awk -F: '$1=="used_memory_startup"{print $2+0}')
  backlog=$(echo "$mem" | awk -F: '$1=="mem_replication_backlog"{print $2+0}')
  keys=$(valkey-cli -h "$h" MEMORY STATS | awk 'p{print; exit} $0=="keys.count"{p=1}')
  echo "$h: $(( (used - start - backlog) / keys )) bytes per key over $keys keys"
done

Interpretation: this is the number to size from. The difference between the two lines is the measured saving per key for your real mix of types, sizes and TTLs, including the hashtable buckets that MEMORY USAGE leaves out.

How do you sample per-key memory with MEMORY USAGE?

The totals tell you how much you save. A key sample tells you where it comes from, which you need when the saving is smaller than expected or one key family grows. Take one list of keys and query both replicas with it.

bash
export LC_ALL=C
valkey-cli -h r-old --scan --count 1000 | head -n 20000 > keys.txt

sample() {  # $1 = host; prints key, bytes, encoding, type
  while IFS= read -r k; do
    printf '%s\t%s\t%s\t%s\n' "$k" \
      "$(valkey-cli -h "$1" MEMORY USAGE "$k")" \
      "$(valkey-cli -h "$1" OBJECT ENCODING "$k")" \
      "$(valkey-cli -h "$1" TYPE "$k")"
  done < keys.txt
}
sample r-old > old.tsv
sample r-new > new.tsv

Interpretation: one client call per key is slow but keeps replica load trivial. MEMORY USAGE samples 5 nested elements by default; keep that on both sides, because SAMPLES 0 reads every element of a large key.

bash
join -t $'\t' <(sort -t $'\t' -k1,1 old.tsv) <(sort -t $'\t' -k1,1 new.tsv) |
awk -F'\t' '$2 != "" && $5 != "" {
  n[$4]++; d[$4] += $2 - $5
  if ($3 != $6) moved[$4 ": " $3 " to " $6]++
} END {
  for (t in n) printf "%-6s %7d keys  mean saving %.1f B/key\n", t, n[t], d[t] / n[t]
  for (m in moved) printf "%s  %d keys\n", m, moved[m]
}'

Interpretation: the first block gives the mean saving per data type. The second counts keys whose encoding changed, such as string: raw to embstr on 9.1 or zset: skiplist to btree on a 9.2 release candidate. Keys that expired between the two runs drop out of the join.

The same data on a 9.0 replica and a 9.1 replica

Illustrative output, not a measurement: totals, then one 12-byte key with a TTL and a 40-byte value

ops@staging-01:~
$ valkey-cli -h r90 INFO memory | grep -E '^used_memory(_startup)?:'used_memory:1094795264used_memory_startup:1024216$ valkey-cli -h r91 INFO memory | grep -E '^used_memory(_startup)?:'used_memory:934795264used_memory_startup:1024216$ valkey-cli -h r90 MEMORY USAGE session:8f3a(integer) 96$ valkey-cli -h r91 MEMORY USAGE session:8f3a(integer) 80

Read the encoding too: a drop like 96 to 80 bytes usually comes with OBJECT ENCODING moving from raw to embstr. These numbers show the shape of the output, so run the commands on your own keys.

When do existing keys pick up the new layout?

When each node loads its data on the new version. The hashtable and key-embedding changes apply as a node builds its keyspace, which on an upgraded node means loading the RDB or AOF at startup. The 9.1 string change is explicit about this: a key becomes embstr under the 128-byte budget when it is written or reloaded, and the 9.1 post notes that "a restart alone is enough to trigger it". A replica that has just done a full sync has re-encoded everything, which is another reason the replica method shows the steady state you will reach.

After production restarts, run OBJECT ENCODING and MEMORY USAGE on a few sampled keys. If they match the 9.1 replica, production has reached the measured state.

The 9.2.0-rc1 notes replace the skiplist for large sorted sets with a B+ tree (btree) and keep small hashes with field expiration as listpacks. Both are 9.2 release-candidate behaviour, with no published per-member figure, so do not plan 9.1 capacity on them.

Which 9.1 patch should the test replica run?

9.1.3 or later. PR #4819, shipped in the 9.1.3 release, fixes a listpack encoding difference that "caused RDB load and replication failures for entries of certain lengths between 9.0 and 9.1.x". The PR says the affected lengths "are reachable in normal use", and that 9.1.0 to 9.1.2 cannot read the 9.0 form. A 9.1.0 replica of a 9.0 primary can therefore fail its full sync partway through, and your measurement never starts.

9.1.3 is also a security release, so plan the production upgrade on it too. The version check in the sync loop above shows what each test replica runs.

The version-by-version load and rollback rules, including what Valkey 8 can and cannot read from a 9.x node, are in the companion post on Valkey RDB versions and upgrade rollback.

How do you turn the measured saving into a node count?

Start from the bytes per key measured on the two replicas and your production key count, then apply the fill target you already run at. The fill target is your existing headroom for fork copy-on-write during snapshots, replication and client buffers, and fragmentation. The upgrade does not change that policy.

bash
# Placeholders: replace every value with your own measurements
awk -v keys=480000000 -v before=182 -v after=151 -v node_gib=52 -v fill=0.75 'BEGIN {
  split(before " " after, bpk, " "); split("before after", label, " ")
  for (i = 1; i <= 2; i++) {
    gib = keys * bpk[i] / 1024^3; s = gib / (node_gib * fill); n = int(s); if (n < s) n++
    printf "%-6s %6.1f GiB  %d primaries at %d%% fill\n", label[i], gib, n, fill * 100
  }
}'

Interpretation: the output is a primary count; multiply by replicas per shard for the node count. If the "after" figure is not at least one whole shard smaller, the upgrade buys headroom rather than a smaller fleet, and that is still worth having.

Measure, compare, right-size

Seven steps; production does not change until step 5

  1. Add a baseline replica

    Same version and configuration as production, attached to one primary in a quiet period.

    valkey-cli -h r-old REPLICAOF \ primary.internal 6379
  2. Add a target-version replica

    9.1.3 or later, same primary, same cluster-enabled setting.

    valkey-cli -h r-new REPLICAOF \ primary.internal 6379
  3. Wait for both syncs

    Both links up, no sync in progress, key counts within normal churn.

    valkey-cli -h r-new INFO replication
  4. Compare bytes per key

    Use used_memory minus startup and backlog, divided by keys.count, then sample keys to explain the gap.

    valkey-cli -h r-new MEMORY STATS
  5. Upgrade production

    Run your normal rolling upgrade. Each restart reloads data and applies the new encodings.

  6. Re-measure after a traffic cycle

    Read the same metrics on production replicas, including month-end or peak jobs.

  7. Change the node count

    Reshard or change node types only if the measured figure still clears your fill target.

Main risk: resizing from the replica figure alone. Production fragmentation, buffers and key growth eat into the saving, so step 6 is the number you resize on.

For an eviction cache at maxmemory, the 9.1 post notes that the saving shows up as a higher hit rate rather than free memory, so check the hit rate before you remove capacity.

Frequently asked questions

How much memory does upgrading Valkey from 7.2 to 8.0 save?

Valkey's own test reports about 20.63% less used_memory for 6.3 million keys with 16-byte values in cluster mode. That comes from 16 bytes per key for per-slot dictionaries, which applies only in cluster mode, plus about 8 bytes from key embedding. Larger values or standalone mode give a smaller share, so measure on a replica.

Does Valkey 9.1 reduce memory usage without configuration changes?

Yes. Valkey 9.1 removes an 8-byte pointer from embedded strings, raises the embedding budget from 64 to 128 bytes, and embeds sorted set members in skiplist nodes, with no new settings. Valkey reports 17 to 44% less string overhead, averaging 26%, and about 6 to 8.5 bytes per short sorted set member. Some lengths see no gain.

Why does MEMORY USAGE report different numbers after a Valkey upgrade?

Partly because the layout changed and partly because the accounting did. In 7.2 and 8.0, MEMORY USAGE adds the dictionary entry and key allocation. From 8.1 it measures the object holding the key and value, without the hashtable bucket slot. Releases 8.1.0 to 8.1.3 also missed the embedded key, fixed in 8.1.4 and 9.0.0.

Can I reduce my Valkey node count after upgrading?

Only after you measure it. Compare bytes per key on two replicas of the same primary, upgrade production, and re-measure after a full traffic cycle. Then apply your existing fill target. If the result does not remove at least one whole shard, keep the saving as headroom. Eviction caches usually gain hit rate rather than spare memory.

Key takeaways

  • Per-key overhead fell in 8.0, 8.1 and 9.1, and each step targets different key shapes: cluster mode, TTL keys, small strings, large aggregates and large sorted sets.
  • Vendor figures come from small synthetic keys. Quote them as Valkey's numbers and size from your own measurement.
  • Compare used_memory minus startup and backlog per key across two replicas of one primary. Do not compare used_memory_dataset or MEMORY USAGE totals across 7.2 or 8.0 and 9.1.
  • Use 9.1.3 or later for any 9.1 node that replicates from or loads data written by 9.0.
  • Resize on the production figure after a traffic cycle, with the headroom policy you already run.

Size your Valkey upgrade with JusDB

JusDB's Valkey performance tuning team can review your key and value shapes, plan and read the side-by-side replica test, and turn the measured bytes per key into a maxmemory and node-count plan for your cluster.

Bring your current version, topology, INFO memory and MEMORY STATS from a replica, and your headroom policy. Your team approves every production change.

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Moving a Multi-Database Valkey Instance into a 9.x Cluster

Move a Valkey instance that uses SELECT 1 to 15 into a 9.x cluster: keep or remap databases, copy each one, verify counts per database, and keep a way back.

Redis18 minNov 10, 2026
Read

Valkey Keyspace Notifications in Production: Expired Events, hexpired and Cluster Mode

Run Valkey keyspace notifications in production: why expired and hexpired events lag the TTL, how to subscribe on every cluster node, and when a stream is safer.

Redis20 minNov 10, 2026
Read

Valkey GLIDE in Production: AZ-Affinity Reads and Migrating from Legacy Clients

Configure Valkey GLIDE AZ-affinity reads on servers and clients, learn what GLIDE 2.5.3 does not support, and migrate from redis-py, Jedis, ioredis, or go-redis one service at a time.

Redis20 minNov 10, 2026
Read