What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Caching is the cheapest performance win there is, and the easiest way to serve stale data. How each cache layer works, which headers to use, and how to invalidate without tears.
There's an old joke that the two hardest problems in computer science are cache invalidation, naming things and off-by-one errors. It's funny because caching really is both the cheapest performance win available and a reliable source of "why am I seeing old data?" bugs.
The trick is knowing which layer to cache at and how each one expires.
A request that's answered by layer 1 never touches the network. One answered by layer 2 never reaches your server. Every layer you skip saves time and money.
The Cache-Control header tells browsers (and CDNs) what they may store and for how long.
Static assets with hashed filenames (app.3f9a1c.js, logo.8b2e.png): cache them forever. When the content changes, the filename changes, so there's nothing to invalidate.
Cache-Control: public, max-age=31536000, immutable
Most modern build tools hash filenames automatically. This one header is often the biggest repeat-visit speedup you can get.
HTML pages: these reference the hashed assets, so they must stay fresh.
Cache-Control: no-cache
no-cache doesn't mean "don't store." It means "check with the server before using it." Combined with an ETag, the server can answer 304 Not Modified with no body, which is fast and cheap.
Private, user-specific responses (account pages, API responses with personal data):
Cache-Control: private, no-store
private keeps shared caches like CDNs from storing them. no-store prevents storage entirely, appropriate for sensitive data.
A CDN stores responses at edge locations around the world. For static sites it's automatic. For dynamic pages and APIs, you control it with headers:
Cache-Control: public, s-maxage=300, stale-while-revalidate=600
s-maxage=300: shared caches (the CDN) may serve this for 5 minutes.stale-while-revalidate=600: after that, serve the stale copy instantly while fetching a fresh one in the background.That combination is wonderful for blogs, product pages and public listings: users always get a fast response, and content is never more than a few minutes old.
Frameworks wrap this in friendlier APIs. Next.js revalidation, for example, is built on the same idea of serving cached content and refreshing it periodically or on demand.
Watch out for: caching responses that vary by cookie, language or device without telling the CDN. Use the Vary header or keep personalised responses out of shared caches.
For expensive computations or slow queries, cache results in your app, usually in Redis or in memory.
The most common pattern is cache-aside:
async function getProduct(id) {
const key = `product:${id}`
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
const product = await db.products.findById(id)
await redis.set(key, JSON.stringify(product), { EX: 600 }) // 10 minutes
return product
}
Check the cache; on a miss, load from the database and store the result with a TTL.
In-memory vs Redis: an in-process cache (a Map or an LRU library) is fastest but isn't shared between server instances and disappears on restart. Serverless functions make in-memory caching unreliable. Redis (including hosted options like Upstash) is shared and survives deploys.
Three strategies, from simplest to most precise:
product:{id} from the cache. The next read repopulates it.product:42:v7). Updating changes the version, and old entries just expire.Combining TTL with delete-on-write covers most applications: fresh after edits, and self-healing if a deletion is ever missed.
When a popular cache entry expires, hundreds of requests can miss at once and all hit the database simultaneously. Defences include:
Track your cache hit ratio. A cache with a 20% hit rate is mostly overhead. Check response headers too: most CDNs add a header that shows whether a response was a HIT or a MISS.
Caching works best on top of a fast baseline. If the underlying query is slow, also look at indexing and the N+1 problem. And for the full performance picture: why is my website slow?
Cache hashed static assets forever, keep HTML revalidating, use CDN caching with stale-while-revalidate for public pages, and use cache-aside in Redis for expensive queries, with TTLs plus delete-on-write.
no-cache allows storing but requires revalidation with the server before each use. no-store forbids storing the response at all.
Usually the HTML is cached too aggressively, or assets don't have hashed filenames. Set no-cache on HTML, hash asset filenames, and purge your CDN cache after deploys if needed.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →