Setting a Redis Eviction Policy for Spatial Payloads
Use allkeys-lru with a maxmemory set below the instance’s real limit — noeviction turns a full cache into write errors, and volatile-* policies silently do nothing to entries you forgot to give a TTL.
Jump to heading Why this matters
A cache holding spatial payloads fills differently from one holding session tokens. Entries are large and unevenly sized — a dense urban region and a sparse rural one differ by an order of magnitude — and the working set grows with the number of distinct viewports analysts happen to visit, which is not a number anybody controls. Sooner or later the instance is full, and what happens next is decided entirely by two configuration values most teams never set.
The default in a stock Redis is noeviction, which does not evict. When memory runs out, writes start failing with an out-of-memory error while reads keep working. For a cache that is a strange failure mode: the dashboard keeps serving from whatever is already cached, new regions can never be cached, and the error surfaces in the write path of a function whose job was to make things faster. Nothing looks broken until somebody notices the cache hit rate has been falling for a week.
The other trap is the volatile-* family, which only considers keys that have a TTL. That is a sensible policy when a cache and a data store share an instance — and a trap when a single forgotten set without an expiry creates an entry the eviction policy is not allowed to touch. A handful of those, each holding a large frame, and the instance is permanently short of the memory the policy was supposed to reclaim.
Jump to heading Prerequisites
- A Redis 6 or 7 instance you can configure, or a managed one whose eviction policy is exposed as a parameter.
- The shared cache from Redis as a shared cache layer, with every write already carrying an expiry.
- A rough figure for your mean entry size, which the WKB serialisation guide shows how to measure.
Jump to heading Step-by-step solution
Jump to heading Step 1 — Set maxmemory below the real limit
Redis does not know how much memory the machine or container has; it evicts against whatever maxmemory says, and if that is unset it will happily grow until the kernel intervenes. On a container, the kernel intervening means an OOM kill, which drops every cached entry at once.
# Leave headroom: the process itself, replication buffers, and fragmentation
# all live outside the accounted dataset.
redis-cli CONFIG SET maxmemory 3gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
Set maxmemory to roughly seventy-five percent of the container limit. The remaining quarter covers the client output buffers — which for spatial payloads are unusually large, since a single reply may be a hundred megabytes — plus replication and allocator fragmentation, none of which count toward the dataset size Redis is measuring.
Jump to heading Step 2 — Choose allkeys-lru, and know why
Of the eight policies, three are plausible for a spatial cache and only one is usually right.
allkeys-lru evicts the least recently used key regardless of TTL. It matches how dashboards are used: analysts return to the regions they work on and drift away from the ones they visited once, so recency is a good predictor of future access. This is the default recommendation.
allkeys-lfu evicts the least frequently used, which is better when a small set of reference layers is hit constantly and everything else is a long tail of one-off viewports. If your access pattern is a handful of national base layers plus scattered exploration, measure it — the difference can be several percentage points of hit rate.
volatile-ttl evicts whatever expires soonest. It sounds elegant and it makes eviction depend on a value chosen for freshness rather than for memory, so a layer given a short TTL because its upstream changes hourly is evicted before a stale one with a long TTL. Freshness and memory pressure are different questions, and conflating them is why this policy usually disappoints.
Jump to heading Step 3 — Give every entry a TTL anyway
Even under allkeys-lru, which does not need TTLs, set one. The eviction policy bounds total memory; the TTL bounds staleness, and they are answering different questions. An entry that is small enough never to be evicted can otherwise persist indefinitely, long after the dataset it was derived from changed.
r.set(key, payload, ex=900) # not just r.set(key, payload)
Make it impossible to forget by putting the expiry in the helper rather than at the call sites, so a set without one cannot be written by accident.
Jump to heading Step 4 — Watch three numbers
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|evicted_keys'
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human|mem_fragmentation_ratio'
The hit rate tells you whether the cache is earning its keep. evicted_keys climbing steadily is normal and healthy; climbing fast relative to writes means the working set does not fit and entries are being discarded before they are reused, at which point you are paying for a cache that mostly misses. And a fragmentation ratio much above 1.5 means the allocator is holding memory Redis is not using, which for the large, unevenly sized values a spatial cache produces is a common and easily misread condition.
Jump to heading Verification
# 1. The policy is what you think it is — managed instances often override it.
redis-cli CONFIG GET maxmemory-policy # expect: allkeys-lru
redis-cli CONFIG GET maxmemory # expect: non-zero
# 2. Nothing was written without an expiry.
redis-cli --scan --pattern 'spatial:*' | while read -r k; do
[ "$(redis-cli TTL "$k")" = "-1" ] && echo "no TTL: $k"
done
The second check is the one worth running on a schedule. A single immortal entry is harmless; a code path that writes them accumulates, and under a volatile-* policy it is the exact leak that makes the eviction policy useless.
Use --scan rather than KEYS: KEYS blocks the server for the whole scan, which on a large instance is long enough to time out every other client.
Jump to heading Edge cases and gotchas
- Managed instances override the policy. Several cloud Redis products ship with
volatile-lruornoevictionby default and reset it on maintenance. Assert the policy at application startup and log loudly if it is not what you expect. - Large values distort LRU. Redis samples a handful of candidate keys per eviction rather than maintaining a true LRU list, so eviction is approximate. With very unevenly sized values that occasionally means freeing several small entries where one large one would have done. Raising
maxmemory-samplesimproves the approximation at a small CPU cost. - The lock keys count too. A stampede guard that writes a lock per key adds a key per entry. They are tiny and short-lived, but on an instance with millions of entries the count matters for the key-space overhead even when the bytes do not.
- Replicas inherit the data, not the policy. Set
maxmemoryon the replica as well, sized for the same dataset, or a failover promotes an instance that will not evict.
Jump to heading FAQ
Should the eviction policy replace TTLs?
No — they answer different questions. The policy bounds how much memory the cache uses; the TTL bounds how stale an answer can be. An entry small enough never to be evicted will otherwise live indefinitely, long after the data it derived from changed, so set both.
How do I know whether the instance is too small?
Compare evicted_keys to the write rate over the same window. Occasional evictions are healthy. Evictions approaching the number of writes means entries are being discarded before anyone reuses them, and the cache is close to pure overhead — which shows up as a hit rate that falls when it should be flat.
Is allkeys-lfu ever better than allkeys-lru?
Yes, when a small set of reference layers is hit constantly and the rest is a long tail of one-off viewports — the pattern of a dashboard with a few national base layers and a lot of exploration. Under LRU a burst of exploration can evict a base layer that is about to be needed again; LFU keeps it. Measure both before committing, since the answer depends on how your analysts actually work.
Back to Redis as a Shared Cache Layer.