Lazy Freeing in Redis: Deleting Large Keys Without Blocking the Server
AI generated
SET
TTL
Redis / Performance Tuning
Lazy Freeing
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.

8 min read Lazy Freeing UNLINK DEL Single Thread Memory Reclamation

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.

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.

11. FAQ: Lazy Freeing

1What is the exact difference between DEL and UNLINK?
Both remove a key immediately and visibly from the keyspace. DEL releases the associated memory synchronously in the main thread, which blocks the server for large data structures. UNLINK also removes the key from the keyspace immediately, but hands the actual memory reclamation off to a background thread, so the main thread can keep processing other commands.
2Is UNLINK always faster than DEL?
From the calling client's perspective, UNLINK returns much faster for large keys, because only the keyspace entry needs to be removed. For small keys, the difference is barely measurable, since memory reclamation only takes nanoseconds there anyway.
3Can I just replace DEL with UNLINK everywhere?
In the vast majority of cases, yes, since both commands behave identically from the caller's perspective. An exception is cases where an application explicitly needs to rely on the memory being fully and immediately released, for example right before a hard memory limit check that follows directly afterward.
4What exactly does lazyfree-lazy-user-del do?
This option makes the classic DEL command internally behave like UNLINK automatically. Existing code that uses DEL then benefits from non-blocking behavior without the code itself needing to change.
5How do I know if the lazy-free background thread is overloaded?
INFO Memory exposes the lazyfree_pending_objects counter. A persistently high or steadily growing value shows that more objects are waiting to be freed than the background thread can process.
6Does FLUSHALL block the server even without any particularly large individual keys?
Yes, if the database as a whole contains a very large number of keys, memory reclamation across all of them adds up to noticeable blocking, even if no single key is huge on its own. The ASYNC argument or the lazyfree-lazy-user-flush option solve this problem.
7Is there a downside to permanently enabling all lazyfree options?
The main downside is a small additional CPU and memory overhead from the extra background thread, plus a brief delay between visible deletion and actual memory reclamation. For the vast majority of production setups, the benefit of avoiding blocking clearly outweighs that cost.
8Does lazy freeing also work with Redis Cluster and replication?
Yes, lazy freeing is a local mechanism of each Redis process and works regardless of whether an instance is part of a cluster or a replication topology. Under replication, the delete command is propagated to replicas and handled there according to their own lazyfree configuration as well.
9What happens if the Redis process crashes while the background thread is still freeing memory?
Since this is purely in-memory management within the same process, this is not a data loss issue: the key was already deleted and removed from the keyspace before the background thread became active. A crash simply terminates the entire process, including its memory, regardless of the reclamation status.
10Since which Redis version are UNLINK and the lazyfree options available?
UNLINK and the basic lazyfree configuration options were introduced in Redis 4.0. The ASYNC argument for FLUSHALL and FLUSHDB has also been available since Redis 4.0, and in current Redis 8 installations all of these mechanisms are naturally present.