deleting large keys without blocking the server
A single DEL command looks harmless, but on a collection with millions of elements it can freeze the entire Redis server for several hundred milliseconds. Lazy freeing moves the actual memory reclamation into a background thread and makes deleting large keys noticeably fast, regardless of their internal size.
Table of Contents
- 1. Why DEL blocks the server on large keys
- 2. How UNLINK solves the problem
- 3. An overview of the lazyfree configuration options
- 4. lazyfree-lazy-user-del: making DEL behave like UNLINK automatically
- 5. When lazy freeing really pays off
- 6. FLUSHALL and FLUSHDB with the ASYNC option
- 7. Interaction with replication and AOF persistence
- 8. Monitoring the lazy-free background thread
- 9. Practical conclusion: establish UNLINK as the default for large keys
- 10. Summary
- 11. FAQ
1. Why DEL blocks the server on large keys
By default, Redis processes commands in a single main thread, which guarantees maximum predictability, because each command runs to completion before the next one starts. For simple operations like GET or SET on small values, that takes only microseconds. But for DEL on a very large data structure, for example a hash with ten million fields or a list with just as many elements, the main thread has to walk through every internal object and release its memory before the command is considered complete.
That is exactly where the single-thread model's weakness shows up: while the main thread is busy releasing memory for this one large key, it cannot process a single other command for any other client, even if that client only wants a trivial GET on a completely unrelated key. From the perspective of every other connection, Redis appears frozen for the duration of that release, which in production causes noticeable latency spikes and, in the worst case, timeout errors in the calling application.
2. How UNLINK solves the problem
UNLINK was introduced as an asynchronous alternative to DEL and behaves identically from the caller's perspective, the key is immediately no longer visible or reachable after the command returns. The crucial difference lies in the internal implementation: UNLINK only removes the key from the keyspace dictionary, which is a fast, constant-time operation, and hands the actual memory reclamation off to a dedicated lazy-free background thread. That thread runs alongside the main thread and frees the memory incrementally, without blocking the processing of new commands.
The distinction between visible deletion and physical deletion matters here: as soon as UNLINK returns, the key is already gone from every client's perspective, a subsequent GET returns nil, an EXISTS returns 0. The actual memory, however, only gets released slightly later by the background thread. For application logic, that difference is entirely invisible, it only affects how quickly the server responds to the delete command itself.
# Blocking delete of a very large hash
redis-cli DEL huge-product-catalog-cache
# Can take several hundred milliseconds with ten million fields
# and blocks ALL other clients during that time
# Non-blocking delete of the same hash
redis-cli UNLINK huge-product-catalog-cache
# Key disappears immediately, memory reclamation runs in the background thread
3. An overview of the lazyfree configuration options
Besides the explicit UNLINK command, Redis offers several configuration options that automatically switch internal deletion operations into lazy-free mode, without requiring the application to call UNLINK itself. lazyfree-lazy-eviction controls whether keys automatically removed under maxmemory pressure are freed asynchronously. lazyfree-lazy-expire affects keys removed because their TTL expired, and lazyfree-lazy-server-del affects internal commands such as RENAME, which first have to remove an old key before the new name takes effect.
A particularly important option is lazyfree-lazy-user-flush, which affects FLUSHALL and FLUSHDB. Without this option, a FLUSHALL on a large database blocks the server for the entire duration of the memory reclamation, which can cause unexpectedly long downtime, especially in test environments or during planned maintenance windows. With the option enabled, the command returns immediately while the actual memory is freed in the background.
# redis.conf: enable lazy freeing for all relevant cases
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
lazyfree-lazy-user-del yes
lazyfree-lazy-user-flush yes
# Check and set at runtime
redis-cli CONFIG GET lazyfree-lazy-user-flush
redis-cli CONFIG SET lazyfree-lazy-user-flush yes
4. lazyfree-lazy-user-del: making DEL behave like UNLINK automatically
An often overlooked option is lazyfree-lazy-user-del. When enabled, the classic DEL command internally behaves exactly like UNLINK, without requiring any changes to client code or existing scripts. This is especially useful for existing applications where DEL is hardcoded in many places and rewriting everything to use UNLINK would be tedious or risky.
The trade-off is that this setting applies globally to the entire server and cannot be controlled granularly per key or per command call. Anyone who needs a deliberately synchronous delete for specific cases, for example because a subsequent operation must rely on the memory being fully and immediately released, should use this option carefully and, when in doubt, keep explicitly distinguishing between DEL and UNLINK rather than relying on global automatic behavior.
5. When lazy freeing really pays off
Lazy freeing shows its value primarily with data structures that contain many internal elements: large hashes with millions of fields, long lists, extensive sorted sets, or sets with a very large number of members. For small keys, for example a simple string of a few bytes, the difference between DEL and UNLINK is practically unmeasurable, because memory reclamation only takes a few nanoseconds anyway and causes no noticeable blocking even when done synchronously.
In a Magento context, this typically involves large full page cache entries with extensive HTML fragments, aggregated product catalog caches, or session containers that accumulate data over long periods. Anyone who regularly invalidates large cache regions, for example after a price import or a category restructuring, should consistently use UNLINK instead of DEL to avoid latency spikes for every other shop visitor active at the same time.
6. FLUSHALL and FLUSHDB with the ASYNC option
In addition to the global lazyfree-lazy-user-flush configuration, FLUSHALL and FLUSHDB have offered an explicit ASYNC argument since Redis 4, which can be set per call independently of the global configuration. This is especially valuable for administrative scripts or maintenance tools that should not assume the server operator has already enabled the matching lazyfree option globally.
The counterpart is the explicit SYNC argument, which forces the command to run synchronously and blocking even when the global lazy-free configuration is enabled. This is useful for test scenarios or automated deployment scripts that must guarantee a database is completely and verifiably empty before the next step begins, for example before importing a fresh dataset in a CI pipeline.
# Non-blocking flush, independent of global configuration
redis-cli FLUSHALL ASYNC
# Forced blocking flush (e.g. for deterministic CI scripts)
redis-cli FLUSHDB SYNC
7. Interaction with replication and AOF persistence
An important aspect often overlooked when switching to UNLINK: the lazy-free mechanism is a purely local implementation detail of the respective Redis process and does not change externally visible replication behavior at all. When UNLINK runs on a primary server, Redis propagates the command unchanged as UNLINK to every connected replica, so each replica also benefits from asynchronous memory reclamation on its own, instead of the command being converted into a blocking DEL.
Something similar applies to AOF persistence: the UNLINK command gets written into the AOF file as such, so replaying the AOF log later, for example after a restart, also uses the non-blocking variant. What matters here is that the actual background memory reclamation, unlike command propagation, happens purely locally on each respective server and does not need to be synchronized between primary and replicas, since every instance manages its own memory independently.
8. Monitoring the lazy-free background thread
Redis exposes a counter called lazyfree_pending_objects via INFO Memory, indicating how many objects are currently waiting to be freed by the background thread. A persistently high or steadily growing value indicates that more keys are being deleted than the background thread can process, pointing to an unusually high deletion rate of large objects or an undersized server configuration.
INFO Memory also shows the current used_memory value, which stays briefly higher than expected after deleting a large key when the lazy-free thread lags behind, simply because the memory has not yet been fully released. When monitoring Redis instances, it is worth including this counter in the regular metrics dashboard, especially in environments with frequent deletion of large data structures.
9. Practical conclusion: establish UNLINK as the default for large keys
For new applications and scripts, it is worth establishing UNLINK directly as the default command for deleting keys, instead of relying on global lazyfree configuration, which can be set differently depending on the deployment environment. For existing code where DEL is used widely, lazyfree-lazy-user-del is a pragmatic way to disable blocking behavior globally without touching every single call site.
It remains important that lazy freeing is not a cure-all: it only moves the cost of memory reclamation into a background thread, it does not eliminate it. Under an extremely high deletion rate of large objects, even the background thread can fall behind, which shows up as a rising lazyfree_pending_objects counter and should serve as a signal to fundamentally review the data model, for example whether individual keys really need to be that large.
| Configuration option | Affects | Effect when yes | Recommendation |
|---|---|---|---|
| lazyfree-lazy-eviction | Automatic eviction under maxmemory | Asynchronous freeing instead of blocking | Enable with large keys |
| lazyfree-lazy-expire | TTL expiration | Asynchronous freeing of expired keys | Enable with large keys |
| lazyfree-lazy-user-del | Classic DEL | DEL behaves like UNLINK | Useful for existing code |
| lazyfree-lazy-user-flush | FLUSHALL / FLUSHDB | Non-blocking flush | Enable in production environments |
| ASYNC argument on FLUSHALL | Single call, overrides config | Non-blocking for that call | For administrative scripts |
Mironsoft
Cache layer setup and Magento Redis integration
Magento cache that isn't quite working or is misconfigured?
We set up Redis as a cache and session backend for Magento cleanly, tune memory usage and eviction strategies, and make sure full page cache and session storage work together reliably.
Redis Setup
Configure the cache, session, and FPC backend production-ready for Magento.
Memory Tuning
Match memory usage and eviction policies to the shop's actual load.
High Availability Setup
Set up Redis Sentinel or Cluster for resilient Magento environments.
10. Summary
Lazy Freeing
DEL blocks on large keys
The single thread waits until the entire memory of a large key has been released.
UNLINK replaces it instantly
The key disappears immediately from view, memory reclamation runs in a background thread.
lazyfree options automate it
Eviction, expiry, flush, and even DEL can be switched globally to asynchronous behavior.
Do not forget monitoring
lazyfree_pending_objects in INFO Memory shows whether the background thread keeps up with the load.