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.

What each policy does at the moment the instance fillsMemory usage climbs toward the configured maximum in all three cases. Under noeviction the line stops at the ceiling and stays there: reads keep working, every write returns an out-of-memory error, and the hit rate decays as the cached viewports go stale and no new ones can be added — a failure that is invisible until somebody plots the hit rate. Under a volatile policy the line drops only slightly, because the reclaim is limited to keys carrying a time-to-live and a few large entries written without one cannot be touched at all; the instance ends up permanently short of the memory the policy was supposed to free. Under allkeys-lru the line saws steadily just under the ceiling as the least recently used entries are reclaimed to make room for new ones, which is exactly the behaviour a cache wants, and the hit rate holds flat because the entries being discarded are the ones nobody is asking for.MEMORY USED, AS THE WORKING SET GROWSmaxmemorynoeviction — flat at the ceiling, every write failsvolatile-lru — reclaims only what has a TTLallkeys-lru — saws under the ceiling, which is what a cache should dodistinct viewports cached →The noeviction line is the dangerous one: reads keep working, so nothing appears broken, and the only symptom is a hitrate that has been sliding for a week.

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.

bash
# 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.

python
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

bash
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.

The cliff at the point the working set stops fittingHit rate is plotted against the ratio of the working set to the instance size. While the working set fits within the instance the hit rate is flat and high, above ninety percent, and evictions are rare because there is room for everything being asked for. As the ratio approaches one the curve begins to bend, and past it the fall is steep rather than gradual: entries are now being evicted before they are requested again, so each eviction converts a future hit into a miss, and each miss writes a new entry that evicts another. The shaded region past the crossing is labelled as churn, where the cache is doing maximum work for minimum benefit and the round trip it adds is no longer paid for by the queries it avoids. The practical reading is that a cache sized slightly too small is not slightly worse than one sized correctly — it is close to having no cache at all while still costing a network hop on every read.HIT RATE AGAINST WORKING SET ÷ INSTANCE SIZE0%50%100%working set = instanceeverything asked for still fitschurn — evicted before reuse0.25×A cache sized slightly too small is not slightly worse than one sized right — past the crossing it approaches having nocache at all, while still charging a network round trip on every read. Where the memory actually goesThe container limit has to cover more than the dataset Redis reports. The accounted dataset is the entries themselves. Client output buffers hold replies in flight, and for spatial payloads a single reply can be a hundred megabytes, so a handful of concurrent large reads is a substantial and invisible addition. Replication buffers hold writes not yet acknowledged by a replica. Allocator fragmentation is the gap between what Redis asked for and what the allocator reserved, and it is unusually large for values of wildly varying size — which is exactly what a mixture of dense urban and sparse rural layers produces. Setting maxmemory equal to the container limit therefore guarantees an out-of-memory kill rather than an eviction.WHAT THE CONTAINER LIMIT COVERSaccounted by Redisentriesnot accountedoutput buffersreplicationfragmentationSet maxmemory to about three quarters of the container limit — the second row is why the remaining quarter is notspare capacity.

Jump to heading Verification

bash
# 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-lru or noeviction by 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-samples improves 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 maxmemory on 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.