HGETALL vs HSCAN: Read Redis Hashes Without Blocking

Published on
July 24, 2026

The outage rarely looks like a Redis problem at first. Latency climbs across every endpoint at once, including ones that never touch the hash in question. Connection pools saturate. Health checks start timing out.

Then someone finds it: an HGETALL on a key that used to hold forty fields and now holds four hundred thousand.

Redis executes commands on a single thread. HGETALL is O(N) in the size of the hash. Put those two facts together and one unbounded read becomes a stop-the-world pause for every client connected to that node. This post covers the four ways to read a Redis hash, when each is appropriate, and how to make the safe choice the default in Java.

Everything here applies to Valkey too. Valkey shares the commands, the complexity characteristics, and — in its own documentation's words — a mostly single-threaded design for command execution, in which only one command runs at a time and a slow request makes every other client wait.

The Four Ways to Read a Redis Hash

CommandReturnsComplexityUse when
HGETOne fieldO(1)You need a single field
HMGETSeveral named fieldsO(N) in fields requestedYou know which fields you want
HGETALLEvery field and valueO(N) in hash sizeThe hash is small and bounded
HSCANA batch, plus a cursorO(1) per callThe hash is large or unbounded

The distinction that matters is in the complexity column. HMGET scales with what you ask for. HGETALL scales with what exists. A hash that grows by a factor of a thousand makes HMGET no slower and HGETALL a thousand times more expensive.

Why HGETALL Is Dangerous

Three things go wrong at once, and only the first is obvious.

It blocks the event loop. Redis processes commands one at a time. While it walks a million-field hash assembling a reply, it is not serving anyone else. Every other client — including ones hitting completely unrelated keys — waits for that command to finish. This is the mechanism behind the "Redis got slow for no reason" incident.

It's worth heading off a common objection here: yes, both Redis and Valkey are multi-threaded now, and no, it doesn't help. The threading added in Redis 6 and refined in Redis 8, and Valkey 8's asynchronous I/O threading, parallelise network I/O — reading from sockets, parsing, writing responses. Command execution itself still runs on the main thread, deliberately, because that's what makes every command atomic without locking. Adding io-threads will not stop one HGETALL from stalling the server. (Valkey has an active proposal to execute read commands on its async threads, so this may soften on newer versions — worth checking against whichever you run.)

The reply has to be buffered. Before anything reaches the network, Redis assembles the response in the client output buffer. A hash holding a million fields with 100-byte values produces a reply on the order of 100 MB, held in memory on top of the hash itself. There's no safety net here by default: client-output-buffer-limit ships as normal 0 0 0, meaning ordinary clients have no cap at all. On a server already under memory pressure, a large HGETALL can be what tips it over.

Then the client has to deserialize it. Your application allocates the entire result at once. In Java that's a Map with a million entries materialised in one go, which is a garbage collection event waiting to happen and, on a memory-constrained container, a plausible OutOfMemoryError.

HKEYS and HVALS carry exactly the same O(N) cost. Reaching for them instead of HGETALL doesn't avoid the problem, it just returns half as much data.

None of this makes HGETALL wrong. On a user profile with a dozen fields it's the right call — one round trip, no cursor bookkeeping. The failure mode is applying it to a hash whose size you don't control.

HGET and HMGET: Read Only What You Need

The cheapest fix is usually the one people skip: don't read the whole hash.

HGET returns a single field in O(1), regardless of how large the hash is:

> HGET user:1001 email
"ada@example.com"

When you need several fields, don't loop over HGET — that's one network round trip per field. HMGET fetches them in a single call, and its cost scales with the number of fields you asked for, not the size of the hash:

> HMGET user:1001 name email role
1) "Ada Lovelace"
2) "ada@example.com"
3) "engineer"

Fields that don't exist come back as nil in position, so the reply always lines up with your request.

This is the answer far more often than people expect. Most code that calls HGETALL needs three or four fields and throws the rest away. If you can name the fields you want, HMGET is strictly better: less work on the server, less data on the wire, less garbage in your heap.

HSCAN: Cursor-Based Iteration

When you genuinely need every field and the hash isn't small, HSCAN walks it in batches. Each call returns a cursor and a slice; you pass the cursor back until it comes home as 0:

> HSCAN user:sessions 0 COUNT 100
1) "1024"                    # cursor for the next call
2) 1) "sess:a1"
   2) "{...}"
   3) "sess:b2"
   4) "{...}"

> HSCAN user:sessions 1024 COUNT 100
1) "0"                       # iteration complete
2) 1) ...

The work is now spread across many short commands that interleave with other traffic instead of one long one that blocks it. Four things are worth knowing before you rely on it.

COUNT is a hint, not a guarantee. It tells Redis roughly how much work to do per call. You may get more or fewer elements back. The default is 10, which is conservative — for a large hash, raising it to a few hundred cuts the number of round trips substantially, at the cost of more work per call.

MATCH filters after retrieval. HSCAN key 0 MATCH "sess:*" filters server-side, which saves bandwidth, but the filter is applied after elements are pulled from the structure. If your pattern matches few fields, most iterations will return empty batches while the cursor still advances. Empty does not mean finished — only a 0 cursor means finished.

The guarantees are weaker than a snapshot. Any field present for the entire iteration is returned at least once. Fields added or removed midway may or may not appear. Fields can be returned more than once, so your consumer needs to tolerate duplicates.

Small hashes ignore all of this. When a hash is small enough to use the compact listpack encoding, HSCAN returns every field in a single call with cursor 0, regardless of COUNT. So on a small hash, HSCAN is effectively HGETALL with extra steps — which is fine, and it's precisely why defaulting to HSCAN costs you nothing.

Redis 7.4 added a NOVALUES option that returns only field names, useful when you're iterating to find keys rather than read data.

So Which Should You Use?

Work down this list and stop at the first match:

  1. You need one fieldHGET.
  2. You can name the fields you needHMGET. This covers more cases than most people assume.
  3. You need everything, and the hash has a hard upper bound you controlHGETALL. A user profile, a config object, a record with a fixed schema.
  4. You need everything and the size is unbounded or user-drivenHSCAN.
  5. You're reading the same hash constantly and it changes rarely → don't read Redis at all. Cache it locally.

The trap is case 3 shading into case 4 without anyone noticing. A hash that holds "a session's items" is bounded until one customer adds fifty thousand of them. If the field count is a function of user behaviour rather than your schema, treat it as unbounded.

Reading Hashes Safely in Java

The O(N) cost is a property of the server. No client library can make HGETALL cheap. What a client can do is stop you reaching for it by accident — and this is where the common Java clients differ sharply.

With Jedis, jedis.hgetAll(key) is the obvious method with the obvious name. Lettuce gives you hgetall(). Spring Data Redis gives you opsForHash().entries(key). In all three, the O(N) call is the path of least resistance, and the cursor-based alternative requires you to know it exists and manage the cursor yourself.

Redisson inverts that. An RMap is a distributed implementation of java.util.concurrent.ConcurrentMap backed by a Redis hash, so the idiomatic Java way to walk it is a plain enhanced for loop:

RMap<String, String> sessions = redisson.getMap("user:sessions");

for (Map.Entry<String, String> entry : sessions.entrySet()) {
    process(entry.getKey(), entry.getValue());
}

That loop issues HSCAN under the hood. entrySet(), keySet(), and values() all return lazily-loaded views backed by a cursor, fetching in batches of 10 by default. The code that looks most natural to a Java developer is also the code that doesn't block the server.

Tune the batch size when you're iterating something large:

// batch 500 fields per HSCAN call instead of the default 10
for (Map.Entry<String, String> entry : sessions.entrySet(500)) {
    process(entry.getKey(), entry.getValue());
}

And filter server-side with a glob pattern, the equivalent of HSCAN ... MATCH:

Set<String> active = sessions.keySet("sess:*", 500);

One caveat on the pattern variants: matching happens on the raw stored bytes, so configure the map with StringCodec for keys or your pattern won't match anything.

HGETALL is still available — it's just something you have to ask for by name:

Map<String, String> everything = sessions.readAllMap();   // HGETALL
Set<String> allKeys = sessions.readAllKeySet();           // HKEYS
Collection<String> allValues = sessions.readAllValues();  // HVALS

The readAll* prefix is doing real work in that API. It tells you what you're about to do.

For the targeted reads, get() maps to HGET and getAll() maps to HMGET:

String email = sessions.get("sess:a1");                       // HGET
Map<String, String> some = sessions.getAll(Set.of("a", "b")); // HMGET

Two Ways to Avoid the Read Entirely

If a map is read constantly and written rarely, the fastest read is the one that never leaves the JVM. RLocalCachedMap keeps a near cache in your application and uses pub/sub to invalidate entries across every instance when they change, serving hot reads up to 45x faster than a network round trip:

RLocalCachedMap<String, String> config =
        redisson.getLocalCachedMap("config", LocalCachedMapOptions.defaults());

String flag = config.get("checkout.v2"); // no network call after the first read

See client-side caching for how invalidation works across a fleet.

The other option is to stop the hash getting large in the first place. RClusteredMap in Redisson PRO partitions one logical map across multiple cluster master nodes, so reads and memory scale horizontally rather than piling into a single hash slot.

A Note on Async and Reactive

Redisson exposes asynchronous, Reactive Streams, and RxJava3 variants of every method. These stop a slow read from tying up an application thread — genuinely useful — but be clear about what they don't do. They do not make the server-side blocking go away. An asynchronous HGETALL on a million-field hash still occupies the Redis thread for the full duration. It just means your thread is doing something else while every other client waits. Fix the access pattern first; concurrency model second.

How to Find the Hashes That Will Hurt You

You don't have to wait for the incident. Four checks, in order of effort:

Scan for large keys. redis-cli --bigkeys samples the keyspace and reports the largest key of each type. It uses SCAN internally, so it's safe to run against production.

redis-cli --bigkeys

Check the suspects directly. HLEN is O(1), so it's free to ask:

> HLEN user:sessions
(integer) 412093

Check the encoding. A hash reporting hashtable rather than listpack has outgrown the compact representation, which is an early signal it's bigger than intended:

> OBJECT ENCODING user:sessions
"hashtable"

Watch the slow log. SLOWLOG GET 10 shows the commands that actually took time. If HGETALL appears there, you have your answer already.

Worth doing this proactively for any hash whose field count is driven by user behaviour. The keys that cause outages are almost never the ones you'd have guessed.

Frequently Asked Questions

Is HGETALL Slow?

Not inherently. HGETALL is O(N) in the number of fields, so on a small hash it's fast and perfectly appropriate. It becomes a problem on large hashes, because Redis is single-threaded and the command blocks every other client for its full duration.

Should I Use HGETALL or HGET?

Use HGET when you need one field and HMGET when you need several — both cost far less than reading the whole hash. Reach for HGETALL only when you need every field and the hash has a size limit you control.

How Many Fields Before HGETALL Becomes a Problem?

There's no single threshold, because it depends on value sizes, network capacity, and how latency-sensitive your other traffic is. A practical rule: if the field count is bounded by your schema, HGETALL is fine; if it's bounded only by user behaviour, use HSCAN. Measure with HLEN and SLOWLOG rather than guessing.

Does HSCAN Return Duplicates?

It can. HSCAN guarantees that any field present for the whole iteration is returned at least once, but the same field may be returned more than once, so your consumer should be idempotent. Fields added or removed during the scan may or may not appear.

What Is the Java Equivalent of HGETALL?

In Redisson, RMap.readAllMap() issues HGETALL, while iterating entrySet(), keySet(), or values() uses HSCAN instead. Jedis exposes hgetAll(), Lettuce hgetall(), and Spring Data Redis opsForHash().entries() — all of which are HGETALL.

Next Steps

For the full command set and how hashes are stored, see what a Redis hash is. For how the other Redis types map onto Java interfaces, see Redis data structures in Java. If you're weighing clients, Jedis vs Lettuce compares the lower-level options, and the Redisson Map documentation covers every read variant in detail.

For workloads that need local caching or data partitioning on top of the same API, Redisson PRO adds them without changing your code.