The shape
You copied something outward to make reads fast: into edge caches, into replicas, into clients. Now it has to stop being served. A post was deleted, a token was revoked, a licence expired, a court order arrived.
There are two ways to stop serving data you have already copied outward, and they work in opposite directions.
Cache purge takes the copy away. You tell every cache holding the item to drop it. The next request finds nothing there and goes back to your origin servers, which no longer have it either, so nothing is served. Purge is cheap when the item is in a few places, and it only works if you reach every one of them.
Denylist push leaves the copy where it is and takes away the permission. You delete nothing. Instead you send every serving node a short list of identifiers it must not serve, and each node checks that list before answering a request. The item is still sitting in the cache. The node just refuses to hand it over.
Purge removes the data, a denylist removes the permission. That sounds like a small difference and it decides how each one fails.
A purge is a one-time message. A node that never receives it keeps serving the item forever. And once the message has been sent, nothing is left on any node recording what was meant to disappear. So finding the node that missed one means asking every node about every item you have ever purged.
A denylist is not a message, it is state that every node holds and can report. Give the list a version number and each node tells you which version it has. A node that misses an update is still serving the item, so the damage is identical, but it is now showing version 41 while everything else shows 42. Same failure, and this time you can see it.
The same choice shows up under other names. Revoking a JWT before it expires. Un-publishing a CDN asset. Pulling a bad package version, or a video, across a global network. All the same decision.
The moves
Both have to reach every serving node before anything changes, and they get there differently.
Purge propagation is a fan-out you do not control. A global CDN purge usually finishes in seconds, sometimes minutes, and the slowest node to reach is what sets that number. The API usually returns as soon as the request is accepted rather than when propagation completes, so the acknowledgement you get is not the guarantee you want.
Denylist propagation is a pull with a version. Nodes fetch the current list on an interval, and every published list carries a version number and the time it was published. Each node reports the version it holds, and one subtraction tells you how stale that node is: the time now, minus the time its version was published. The answer is in seconds, and it is how far behind that node's copy of the denylist has fallen. The worst one in the fleet is a single number you can alert on and put in a runbook. You can say "no node serves content more than 30 seconds after revocation" and defend it, which you cannot say about purge.
The denylist has to stay small, and that constrains what it can express. It works for a bounded set of recently revoked identifiers. It does not work as a permanent record of everything ever deleted, because every serving node holds the whole thing. The standard resolution is a hybrid: push to the denylist immediately for fast, bounded revocation, purge and delete underneath for permanence, then expire entries from the denylist once the purge is confirmed and no cache could still hold the item.
That hybrid is what most real systems run, and saying out loud why both halves are needed is what the question is testing.
How to tell you have this problem
You have any cache with a long TTL in front of data whose permissions can change. Long TTLs are chosen for hit ratio, and every one of them is a window during which a revocation is not in effect.
Your deletion story ends at the database. If "delete" means removing a row and nobody has written down what happens to the copies at the edge and on devices, the copies are still being served and nobody has measured for how long.
You cannot state a revocation bound. The test question: how long after pressing delete can the content still be served, at p99, and what evidence supports that number? If the answer is "the purge should handle it", you have no bound and nobody has checked.
You have already discovered that immutable caching works against you here. Immutable, long-lived URLs are what make a cache work, and they are exactly what makes revocation hard. You cannot have one without the other, so say which side you took and what it cost you.
What it costs
Neither mechanism reaches a client that already has the bytes. A browser, an app cache, or a downloaded file is beyond any purge or denylist, and no amount of either changes that. If someone actually needs the content to become unreachable, the change has to happen before the bytes ever go out. Two ways to do that. Short-lived signed URLs, which stop working a few minutes after you hand them out, so an old copy expires on its own. Or content you never serve unencrypted, so a copy someone kept is useless without a key you can take away. Both lower your cache hit ratio, and a lower hit ratio means more traffic at your origin servers and a bigger bill. Pick one at design time, because neither can be added afterwards for bytes that are already out.
A denylist is a check on the read path. It runs on every request, so it must be a memory lookup, not a network call. That means it is a per-node data structure to keep fresh, size, and monitor, which is a small distributed system of its own.
Purge is often rate-limited and sometimes billed. Providers limit purge rates, especially for wildcard or prefix purges. A mass deletion of a million objects is not one operation; it is a job with a throughput ceiling that may take longer than the compliance window you are working to.
Staleness monitoring is the part people skip. A denylist gives you a bound only if you actually measure the oldest version in the fleet. Without that measurement it has the same silent failure mode as purge, and you have taken on the complexity without buying the guarantee.
Where it shows up in the course
| article | the form it takes there |
|---|---|
| Caching Strategies and Invalidation | the three ways to invalidate, and the case where the reason is revocation rather than change |
| CDNs and Edge Compute | purge as an invalidation strategy, and why coupling it to the write path is a global fan-out |
| deletion against a one-year immutable max-age, where the purge reaches the edge but not the device, so the answer has to be that the bytes stay reachable | |
| URL Shortener | a phishing takedown against 300 caches holding the mapping forever, where a purge fails silently and a denylist version can be alarmed on |
| YouTube | a takedown against year-long segment caching, where purge alone cannot give you a bound you can state |
Getting it wrong
Treating a purge acknowledgement as completion. The API confirms acceptance, not propagation. If you need a bound, measure propagation separately.
Calling a delete done when only your own copy is gone. Deleting the row on your origin servers does not touch the copies at the edge, and it does not touch the one on somebody's phone. Say how long the content can still be served after a delete, and say plainly that phones and browsers are not covered by that number at all.
Letting the denylist grow without bound. It lives on every serving node. Entries must expire once the underlying purge and delete are confirmed, or it eventually costs memory and a lookup on every single request.
Checking the denylist over the network. A per-request round trip on the hot path defeats the purpose of the cache you were protecting. It has to be local memory.
Not monitoring denylist version age. Without that number the mechanism has purge's silent failure and none of its simplicity.
Assuming short TTLs are a cheaper answer. They are an answer, but the cost is a permanently lower hit ratio applied to all content, in exchange for faster revocation of the tiny fraction that gets revoked. Do that arithmetic before choosing it.