Basis Lab

Answers · The systems around the model

What's the difference between a CDN cache and caching inside my own app?

In shortA CDN cache sits near the user and can only hold answers that are identical for everyone, while a cache inside your app can be personal but exists once per process.

A CDN cache sits outside your infrastructure and near the person asking, so it can only judge a request by what is on the wire: the address, the method and a few headers. A cache inside your own process sits behind the network and can key on anything the request carries, including who sent it. That is the difference that matters. Both hold copies to save work, but they can see different things about a request, and what a cache can see decides what it is allowed to hold.

Reading the ladder of places a copy can live, from the user's own device down through a point of presence, a service process, a shared tier, then the store, each step down is slower to reach and serves more people. Position is not a deployment detail. It sets the unit of sharing.

What the distance actually costs

A request answered by a point of presence near the user costs 18 milliseconds. The same request crossing to the data centre costs 112 milliseconds, so 94 of those milliseconds are geography, and nothing you do inside the data centre touches them.

Now put a cache inside the process. A hit there costs about 0.0002 milliseconds, because it is a pointer follow with no socket and no serialisation. The 0.4 milliseconds to the shared tier is a real round trip by comparison. But the hit in the process is only reached after the 112 milliseconds have been paid, so it saves 0.4 milliseconds out of 112.4, not 112.

Lprocess only=0.9×112.0+0.1×112.4=100.8+11.24≈112.0 msLwith a CDN=0.7×9+0.3×112.4=6.3+33.72=40.0 ms\begin{aligned} L_{\text{process only}} &= 0.9 \times 112.0 + 0.1 \times 112.4 = 100.8 + 11.24 \approx 112.0\ \text{ms} \\ L_{\text{with a CDN}} &= 0.7 \times 9 + 0.3 \times 112.4 = 6.3 + 33.72 = 40.0\ \text{ms} \end{aligned}
FIG 1Two layers, and which one dominates
the average time a user waits for an answer
the share of reads the nearest layer answers
the time the nearest layer takes when it has the answer, here 9 milliseconds
the share of what gets past the nearest layer that the next layer answers
the time the next layer takes when it has the answer
the time the store takes when nothing has it
Everything below the outermost layer is multiplied by the share that got past it. At p = 0.7, a millisecond spent deeper in the stack counts for 0.3 of a millisecond of what the user waits, which is why the edge takes the average from 112.0 to 40.0.

The other cost of living inside a process is multiplicity. Forty processes means forty copies, forty expiry schedules and forty things an invalidation has to reach, and no copy knows what the others hold. A popular item that goes cold produces forty fetches to the store rather than one, because each process misses on its own. Two users asking the same question can land on different processes and get answers of different ages, which produces a bug report nobody can reproduce.

The natural guess is that these are the same cache in different places, so the only question is which one is faster. That guess fails on the first request that differs per user. A CDN cannot tell who is asking unless you put the identity into the address or a header, and the moment you do, the copy is no longer shared and the edge stops being able to help. A cache in your process has the identity and can key on it, which is exactly why it can hold a personal answer, and exactly why it cannot be purged in one place. One purge at the edge reaches every copy of that entry, because there is at most one copy per point of presence.

Splitting rather than choosing

The useful move is usually not to pick a rung but to split the answer across two. There are 94 milliseconds of pure geography between the user and the data centre. If the shell of the page, the part every user receives identically, is held at the point of presence, that shell arrives in 18 milliseconds, and only the small personal remainder travels the 94. The page becomes fast for everyone without the edge ever holding a personal answer.

Two questions settle which part goes where. Who else wants this answer, and how old may it be? Something wanted by everybody that may be minutes old belongs as far out as it will go. Something wanted by one person that must be current stays close in, or is not cached at all.

The in-app cache stops being the wrong choice when the item is personal, changes on a timescale of seconds, and you cannot afford even the 0.4 millisecond shared-tier round trip. It also degrades in a specific way as you scale: each local cache sees a smaller slice of the total traffic, warms more slowly, and duplicates the misses of its neighbours, so a layer that absorbed nearly everything at one process absorbs a fraction of it at forty. The edge has the opposite failure. It stops being usable the moment a response varies on a cookie, on an authorization header, or on anything else that changes per request, because then no two users can share it and there is nothing for it to hold.

Worth keeping

  • A cache near the user removes the distance, but it can only hold what every user would receive identically.
  • A cache inside a process is free to read and comes in as many copies as you have processes, so one invalidation has to reach all of them and each expires on its own clock.
  • The usual answer is to hold the shared part of a page far out and the personal part close in, rather than finding one home for the whole response.

Where this goes next

This question is one step of Keeping the Answer Nearby, a course that works through its subject from the beginning. The page that teaches this part of it is Where to Put It, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. What's the difference between LRU and LFU, and which one should I use?LRU drops the item unused for longest; LFU drops the item requested least often. Both guess at future use, and the cheap win is refusing one-off arrivals rather than counting requests.
  2. Why can a higher hit rate still mean the cache is making things slower?Hit rate counts hits equally and ignores what each hit costs, so a cache with many cheap hits can save less than one with few expensive hits, and can even add more work than it removes.
  3. Should the cache go in front of my app or between the app and the database?Put the shared cache between the app and the database by default, and push a copy in front of the app only for the part of a response that every reader gets identically.
  4. Why can't a system just stay consistent and available when the network splits?During a split, a cut-off machine can only answer from the data it already holds, so it must either risk being wrong or refuse to answer.
  5. How can I tell if my traffic is bursty or just high volume?Compare the variance of the gaps between arrivals with the square of their average gap: high volume alone does not make traffic bursty.
  6. Why does average latency hide the slow requests users actually feel?Waiting times are lopsided, so the average sits between the common case and the bad case and describes neither, and it is the bad case that users keep meeting.

Read alongside