Redis 8.10 shipped in July with a feature called compact hashes: if many hashes share the same field names, Redis can store those field names once instead of once per key. The release blog claims up to 50% lower memory and 2x higher loading throughput. I wanted to know what "up to" actually means for a normal schema, and what you have to do to get it, because the answer to the second question turned out to be less obvious than I expected.
I ran Redis 8.10.1 in Docker and tested it against two schemas, a throughput benchmark, a concurrency check, and the two ways to actually trigger the new encoding: the new HIMPORT command, and a server restart.
The straightforward result
Redis 8.10 adds hash templates: an internal encoding where hashes with the same field names keep one shared copy of those names. The only documented way to create one directly is HIMPORT, a new command pair that declares a field list once per connection and then loads values against it:
HIMPORT PREPARE u name email country last_login
HIMPORT SET user:1 u Alice alice@example.com UK 2026-07-14
HIMPORT SET user:2 u Bob bob@example.com US 2026-07-14
I loaded 100,000 hashes with a four-field user-profile schema (name, email, country, last_login) two ways: plain HSET per key, and HIMPORT against a prepared fieldset. I measured used_memory from INFO memory before and after each load, flushing the database between runs.
| Method | Bytes/hash | Reduction |
|---|---|---|
| Plain HSET | 159.9 | — |
| HIMPORT | 127.6 | 20.2% |
Nowhere near the advertised 50%. So I built a second schema designed to favour the feature: eight fields with longer names (account_status_code, two_factor_enabled_flag, and so on) but short single-character or short-word values, the shape you'd get from flags and codes rather than free text.
| Method | Bytes/hash | Reduction |
|---|---|---|
| Plain HSET | 271.3 | — |
| HIMPORT | 87.3 | 67.8% |
So the 50% figure is workload-dependent in both directions: my ordinary schema landed well under it, my field-name-heavy schema went well over it. The saving comes specifically from field names, so the more your schema's memory is field names rather than values, the more you get back.
The number that didn't hold up
The blog also claims "up to 2x higher hash loading throughput" for HIMPORT versus per-key HSET. I loaded 500,000 hashes both ways, three times each, timed from inside the container with bash's time builtin to avoid docker exec overhead skewing the numbers:
| Method | Fastest of 3 runs | Hashes/sec |
|---|---|---|
| Plain HSET | 2.698s | 185,323 |
| HIMPORT | 1.992s | 251,004 |
That's a 35% improvement, not 2x. I don't know what workload produces the blog's number, maybe a wider schema, maybe a network-bound client where skipping repeated field names in the wire protocol matters more than it does over a Unix socket to a local container. What I can say is that on a bulk pipe load with redis-cli --pipe, the throughput gain was real but far more modest than advertised.
Plain HSET gets nothing, ever, until you restart
This was the part I hadn't expected going in. I loaded the same 100,000-hash, shared-schema dataset with ordinary HSET calls, then checked INFO stats:
hash_templates:0
hash_template_keys:0
Zero. Redis does not look at a live hash, notice it shares a field set with a thousand others, and fold it into a template. That only happens two ways: through HIMPORT, or by reloading an RDB file with three new config parameters set, all of which default to off:
hash-rdb-load-min-template-entries 0
hash-rdb-load-max-template-entries 0
hash-rdb-load-template-disassembly-threshold 0
I set these to 3, 20 and 2 and restarted the container expecting the conversion to happen. It didn't, and hash_templates stayed at 0. The reason turned out to be my mistake, covered below. Once I fixed it, a BGSAVE followed by a container restart converted all 100,000 plain-HSET hashes into a single shared template, and used_memory dropped from 17,459,464 to 14,182,656 bytes, an 18.8% reduction, consistent with the direct HIMPORT comparison on the same schema.
The practical upshot: if your application already writes hashes with plain HSET and a consistent schema, compact hashes give you nothing until you either rewrite your write path to use HIMPORT, or turn on the RDB-load config and restart (or fail over) every node that holds the data. A long-lived primary that never restarts and never uses HIMPORT sees zero benefit from this feature, indefinitely.
Small numbers of shared keys make things worse, not better
A template has its own fixed overhead, and something has to pay for it. I varied the number of keys sharing one fieldset and read MEMORY USAGE on the first key each time:
| Keys sharing the template | MEMORY USAGE (bytes) |
|---|---|
| 2 | 156 |
| 3 | 129 |
| 4 | 116 |
| 5 | 108 |
| 20 | 84 |
| 100 | 77 |
| 1,000 | 76 |
| 5,000 | 76 |
An equivalent plain hash with the same fields reports 112 bytes. So with two or three keys sharing a template, you're worse off than not using the feature at all. The crossover is around four to five keys, and the saving plateaus by about a hundred. If you're grouping small, short-lived batches of hashes under one fieldset, this feature can quietly cost you memory rather than save it.
What happens if you mutate a templated hash
I ran HSET to add a field that wasn't in the schema on one of my 1,000-key templated hashes, and HDEL to remove a field on another:
> HSET user:1 extra_field zzz
(integer) 1
> MEMORY USAGE user:1
(integer) 265
I expected the key to fall back to being a normal hash. It didn't. INFO stats showed hash_templates go from 1 to 2, then to 3 after the HDEL, while hash_template_keys stayed at 1,000 throughout. Redis had forked off a brand new, single-key template for each mutated hash rather than detaching it back to plain encoding. user:1 went from 76 bytes to 265, and the HDEL'd key went to 208, both worse than the 112-byte plain hash baseline. The other 998 untouched keys stayed at 76 bytes, unaffected. So a single stray field on a templated hash doesn't just lose the sharing benefit for that key, it costs more than never having used the feature at all.
Refusals and connection scoping
HIMPORT's fieldsets live on the connection, not the server, and Redis enforces this strictly. Sending the wrong number of values:
> HIMPORT PREPARE u2 name email country last_login
OK
> HIMPORT SET user:3 u2 Carol carol@example.com
(error) ERR value count does not match fieldset field count
Referencing a name nobody prepared:
> HIMPORT SET user:4 doesnotexist X Y Z W
(error) ERR no such fieldset
And, importantly, opening a fresh connection and trying to use a fieldset prepared on another one fails with the same error. I confirmed this by preparing a fieldset in one redis-cli session and calling HIMPORT SET against it from a second, separate redis-cli invocation. Any connection-pooled client, which most production Redis clients are, will hit this the moment a request lands on a different pooled connection than the one that ran PREPARE. You'd need to re-run PREPARE on every connection in the pool, or pin the whole import to a single connection. RESET also drops the fieldset immediately, which I checked directly.
Concurrency behaved better than I expected
I ran ten separate redis-cli --pipe processes in parallel, each opening its own connection, each declaring the same four-field schema under its own PREPARE, each writing 2,000 disjoint keys. All ten finished with zero errors in under a third of a second combined. The interesting part was in INFO stats afterwards:
hash_templates:1
hash_template_keys:20000
One template, not ten. Even though each connection prepared its fieldset independently, Redis deduplicated them server-side by the actual field names, not by the client-chosen fieldset label. Ten unrelated connections converged on one shared template with no coordination required and no errors under concurrent writes.
What I got wrong on the way
My first attempt to test the RDB auto-conversion config failed silently. I set the three hash-rdb-load-* parameters with CONFIG SET, ran BGSAVE, then did docker restart on the container. After the restart, hash_templates was still 0, and the config values had reverted to 0 as well. I assumed the container had picked up the same settings, since docker restart re-runs the same command. It does, but I'd started the original container with only --save "" --appendonly no, no config file and none of the template flags on the command line, so CONFIG SET values I'd applied at runtime were never persisted anywhere Redis would read them again. docker restart doesn't remember CONFIG SET history. I had to stop the container, remove it, and start a new one with the three parameters passed directly as redis-server arguments before the RDB reload conversion worked at all.
Run it yourself
This loads 100,000 hashes both ways and prints the memory delta for each.
docker run -d --name redis810 -p 16379:6379 redis:8.10.1 \
redis-server --save "" --appendonly no
python3 -c "
for i in range(100000):
print(f'HSET plain:{i} name User{i} email user{i}@example.com country UK last_login 2026-07-14')
" > plain.txt
python3 -c "
print('HIMPORT PREPARE u name email country last_login')
for i in range(100000):
print(f'HIMPORT SET himp:{i} u User{i} user{i}@example.com UK 2026-07-14')
" > himp.txt
docker exec redis810 redis-cli FLUSHALL
docker exec redis810 redis-cli INFO memory | grep ^used_memory:
cat plain.txt | docker exec -i redis810 redis-cli --pipe
docker exec redis810 redis-cli INFO memory | grep ^used_memory:
docker exec redis810 redis-cli FLUSHALL
docker exec redis810 redis-cli INFO memory | grep ^used_memory:
cat himp.txt | docker exec -i redis810 redis-cli --pipe
docker exec redis810 redis-cli INFO memory | grep ^used_memory:
docker exec redis810 redis-cli INFO stats | grep hash_template
I ran this exact script against Redis 8.10.1 while writing this post; the numbers match the first table above within a few percent between runs.
What to do with this
If you're already running a schema where thousands of hashes share the same fields, HIMPORT is worth adopting for bulk loads and ETL paths specifically, not as a drop-in replacement for HSET in your normal write path. Check your actual field-name-to-value ratio before trusting the 50% figure either way. If you can't change your write path, the RDB-reload config is a lower-effort option, but remember it does nothing until the next restart or failover, and it needs the three hash-rdb-load-* parameters set explicitly since none of them default to on. Either way, watch INFO stats for hash_templates and hash_template_keys before and after, because at low key-sharing counts, or after a single stray HSET on one of the keys, the feature can leave you worse off than not using it.
Top comments (0)