Right, let’s talk about getting your fresh content to the world, or more accurately, getting the old, cached content out of the world. This is cache invalidation, one of the two hard problems in computer science (the others being naming things and off-by-one errors). CloudFront is a brilliant, global-scale caching machine, and like any good cache, it holds onto things. Your job is to tell it when to let go.

The first and best line of defense is your Time to Live (TTL). This isn’t a CloudFront-specific setting; you configure this on your origin server (like S3 or your custom server) using the Cache-Control max-age directive or the Expires header. CloudFront respects these because it’s polite. When a viewer request comes in, the edge location checks its cache. If the object is there and hasn’t expired (based on your TTLs), bam, it’s served from the edge. This is the performance win you paid for. If it’s stale or missing, CloudForward goes back to your origin, gets the fresh object, and serves it, storing a copy for the next person.

Think of TTLs as your automatic, background janitor. They constantly clean out old stuff on a schedule you define. A high TTL (like 24 hours) is great for static assets that never change—your logo, CSS, fonts. A low TTL (like 60 seconds) is for semi-dynamic content where you can tolerate a brief delay in updates. But sometimes, the janitor’s schedule isn’t enough. Sometimes you drop a new index.html and you need it live now. That’s when you call in the wrecking ball: a manual cache invalidation.

The Nuclear Option: Manual Invalidation

You tell CloudFront to evict one or more objects from its cache immediately, before their TTL expires. You do this by sending an invalidation request, specifying the paths to nuke. The most important thing to know? This costs money. After the first 1,000 free invalidation paths per month, you pay per path. So don’t get trigger-happy.

# Invalidate a single file
aws cloudfront create-invalidation --distribution-id E1A2B3C4D5E6F7 --paths "/images/hero.jpg"

# Invalidate everything (use with extreme prejudice)
aws cloudfront create-invalidation --distribution-id E1A2B3C4D5E6F7 --paths "/*"

The /* path is the “I regret everything” button. It invalidates every single object in your distribution. It works, but it’s expensive, and it hammers your origin server as every single edge location suddenly needs to refetch everything on the next request. Use it only when you’ve truly borked a release across the board.

The Smarter, Cheaper Way: Versioned Paths

The pros almost never use manual invalidations for day-to-day releases. Why? Because we’re cheap and smart. We version our filenames.

Instead of uploading a new file to app.js, you upload it to app.v2.js and update your HTML to reference the new path. Since it’s a全新的 (brand new) path, there’s no cache to invalidate. The first request just misses and gets the new file. This is cache invalidation for free. It’s the best practice for all your static assets.

<!-- Instead of this -->
<script src="/js/app.js"></script>

<!-- Do this (and automate the version number in your build process) -->
<script src="/js/app.2a1b3c.js"></script>

You can use a query string parameter too (app.js?v=2), but you have to configure CloudFront to forward query strings to your origin for caching, which adds a bit of complexity. The path-based versioning is cleaner and more explicit.

The Quiet Annoyance: Cache Behaviors and Default TTLs

Here’s a gotcha. Your origin’s headers are the primary source of truth, but CloudFront has settings that can override them if you’re not careful. In a Cache Behavior, you can set a Minimum TTL, a Default TTL, and a Maximum TTL.

If your origin doesn’t send any Cache-Control or Expires headers, CloudFront will use the Default TTL you set. But if your origin does send a header, the Minimum TTL and Maximum TTL act as clamps. Say your origin says max-age=300 (5 minutes), but you’ve set a Minimum TTL of 3600 (1 hour) in CloudFront. Guess what? CloudFront will ignore your origin and cache that object for the full hour. This is a fantastic way to accidentally make your dynamic content very, very static. My advice? Unless you have a very specific reason to override your origin, leave the Default TTL at 0 and the Min/Max TTLs unchecked. Let your origin be the boss.

The Instantaneous (But Complex) Alternative: Lambda@Edge

Sometimes, even a low TTL isn’t fast enough. For truly dynamic content that must be absolutely fresh on every request, you can use a Lambda@Edge function on the viewer request trigger to effectively set the TTL to zero. The function can bypass the cache entirely for specific paths or based on request headers, forcing a trip back to the origin every time.

// Example Lambda@Edge function to bypass cache for a specific path
exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const uri = request.uri;

    // Check if the request is for our always-dynamic endpoint
    if (uri.startsWith('/api/real-time-stock-data')) {
        request.headers['cache-control'] = [{ key: 'Cache-Control', value: 'no-cache' }];
    }

    callback(null, request);
};

This is powerful, but now you’re introducing code execution at the edge, which adds latency and cost. It’s a trade-off. Use it for the critical stuff, not for everything.

So, to sum up: Version your files. Use sane TTLs from your origin. Treat manual invalidations like a fire alarm—only for real emergencies. And for the love of all that is holy, check your Cache Behavior TTL settings before you deploy to production. Your origin server will thank you.