CDN Smoke Testing After Deployment: Quick Validation That Nothing Is Broken

We pushed a config change at 2:17 AM that looked perfect in staging. Health checks were green, the deploy pipeline said success, and I went to make coffee. By 2:19 AM, half our image URLs were returning 302s to the origin because someone had flipped a cache rule from "ignore query string" to "cache every query string variation." It took four minutes to catch, but those four minutes cost us a full edge purge and a very grumpy on-call page. That's why I don't trust deploy status alone anymore — I trust smoke tests.

Why a Green Deploy Doesn't Mean Your CDN Is Actually Working

When you're just starting out with CDNs, it's easy to assume that if your CI/CD pipeline says "deployed" and your monitoring dashboard shows no 5xx spike, everything's fine. I've made that assumption before and it burned me. A CDN has a lot of layers — DNS, TLS, caching rules, origin shielding, header rewrites, WAF policies — and a tiny mistake in any of them won't always trigger an alert right away. The edge can still serve *something*, just not what you expected.

Smoke testing is the quick sanity check you run immediately after a deploy to answer one question: "Is the most critical stuff still working?" It's not a full regression suite and it's not a load test. Think of it as checking that the lights turn on after an electrician leaves. You don't test every outlet — you just make sure you didn't lose power to the whole house. For a CDN deploy, that means hitting a handful of representative URLs from multiple edge locations and verifying status codes, headers, and basic content integrity before you walk away.

I learned this the hard way back in 2018 when we migrated an e-commerce client to a new edge configuration. The synthetic monitor we relied on was checking /healthz, which was explicitly bypassed from cache. So it kept returning 200 while the actual product pages were serving stale 60-second-old cart counts because we'd set the wrong Cache-Control. The HTTP Working Group Specifications are pretty clear about how Cache-Control and Age should behave, but it's easy to misconfigure them at the edge. A two-minute smoke test that checked a real product page would've caught it instantly. If you're building more comprehensive checks later, you'll want a proper CDN Integration Testing Framework Design: Validating Edge Behavior Programmatically, but for right now, a quick smoke test is your safety net.

#!/bin/bash
# cdn-smoke-basic.sh - Run this immediately after any CDN deploy
# Checks 5 critical URLs for status, cache header, and TLS

URLS=(
  "https://www.example.com/"
  "https://www.example.com/static/app.css"
  "https://www.example.com/api/healthz"
  "https://www.example.com/images/hero.jpg"
  "https://www.example.com/product/12345"
)

for url in "${URLS[@]}"; do
  echo "Checking $url ..."
  curl -s -o /dev/null -D - "$url" \
    -H "Cache-Control: no-cache" \
    --connect-timeout 5 --max-time 10 \
    -w " -> %{http_code} time:%{time_total}s\n" | head -n 20
  
  # Fail fast on non-200/304 for static assets
  status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
  if [[ ! "$status" =~ ^(200|304)$ ]]; then
    echo "FAIL: $url returned $status"
    exit 1
  fi
done
echo "Basic smoke test passed."

The 5-Minute Checklist I Run After Every Single Deploy

I've got a short checklist I run manually or from a post-deploy hook, and it hasn't changed much in five years because it catches about 90% of the dumb mistakes. First, I don't just test from my laptop. Your laptop hits one PoP. Your users hit dozens. I use curl with specific headers, and I'll also run the same checks through a tool like WebPageTest or just from two different cloud regions if I can. The articles on web.dev Articles do a nice job explaining why TTFB and cache status matter for real users, and that's exactly what we're validating here.

Second, I check headers, not just status codes. A 200 that says x-cache: MISS on an asset that should be a HIT is a warning sign. So is a missing strict-transport-security header or a cache-control: no-store where you expected public, max-age=86400. I also verify that the edge isn't stripping or adding headers you need — I've seen CORS break because an edge rule removed Access-Control-Allow-Origin on a deploy. And don't forget to check that your origin is actually being shielded; if every request is a MISS, you haven't broken caching, you've just turned your CDN into an expensive proxy.

This is different from full End-to-End CDN Testing: Building Production-Like Test Environments where you'd spin up a whole staging edge and replay traffic. Smoke tests are intentionally dumb and fast. They shouldn't need a test environment — they run against production, right after deploy, and they should finish in under a minute. If they take longer than your deploy, you won't run them consistently, and that's when things slip through. I keep mine in a single shell script that our CI calls automatically, so nobody has to remember to run it.

# Check headers that actually matter post-deploy
curl -s -I "https://www.example.com/static/app.css" | grep -i -E "^(http|cache-control|age|x-cache|strict-transport|access-control)"

# Example healthy response you WANT to see:
# HTTP/2 200
# cache-control: public, max-age=31536000, immutable
# age: 4123
# x-cache: HIT
# strict-transport-security: max-age=63072000; includeSubDomains; preload

# Purge and re-check if you suspect stale config
# curl -X PURGE "https://www.example.com/static/app.css" -H "Authorization: Bearer $PURGE_TOKEN"
# curl -s -I "https://www.example.com/static/app.css" | grep -i "x-cache"
# Should be MISS on first request after purge, then HIT on second

# Test from a second region/pop using curl with resolve or via different edge hostname
curl -s -I "https://www.example.com/" --resolve "www.example.com:443:151.101.1.1"

Reading the Results: What "Pass" Actually Looks Like

A smoke test is useless if you don't know what numbers should make you stop and roll back. Beginners often look at a 200 and move on, but I look at four things together: status code, cache status, time to first byte, and content validation. You don't need fancy tooling for this. A simple curl with -w gives you timing, and a quick check for a string in the HTML body tells you you're not serving an error page with a 200 status — yeah, that happens.

Here's the table I share with every new engineer on my team. These are the thresholds we use for a smoke test after a routine config or code deploy. They're not strict SLAs and they're not meant for CDN Performance Testing Methodology: Load Generation, Measurement, and Analysis — that comes later when you're doing capacity planning. These are just "does this look normal enough to not wake anyone up" numbers. If any of these fail on two or more URLs, we don't investigate, we just roll back and then look.

One tip I've picked up: always include one uncacheable API endpoint and one heavily cached static asset in your list. If both are slow, it's probably origin or network. If only the cached asset is slow or shows MISS every time, it's your cache key or rule set. That distinction saves you a lot of guessing at 2 AM. Run the same five URLs before and after the deploy so you have a baseline — even saving the output to a file and diffing it is better than relying on memory.

Signal to Check Healthy Smoke Test Result Roll Back If... How to Check Quickly
HTTP Status Code 200 for pages/assets, 304 for revalidation Any 5xx, or 4xx on previously 200 URLs curl -s -o /dev/null -w "%{http_code}" URL
Cache Status (x-cache / age) HIT on 2nd request, age > 0, HIT ratio > 85% on static All MISS, or HIT ratio < 50% on static assets curl -s -I URL | grep -i "x-cache\|age"
Time to First Byte (TTFB) < 150ms for HIT, < 600ms for MISS (global avg) HIT TTFB > 400ms or MISS > 1500ms consistently curl -w "TTFB:%{time_starttransfer}s\n" -o /dev/null -s URL
Content Integrity Body contains expected string e.g. <title> or checksum match Body empty, < 500 bytes, or missing key string curl -s URL | grep -q "Expected Text" && echo OK
TLS / Security Headers HTTP/2 or HTTP/3, HSTS present, cert valid > 14 days TLS error, HSTS missing, or cert expiry warning curl -s -I -v URL 2>&1 | grep -E "SSL|Strict-Transport"

Automating It: A 90-Second Smoke Test You Can Drop Into CI

Look, nobody's got time to run curl manually against 12 PoPs after every deploy. If it's not automated, it won't happen at 2 AM when you're pushing a hotfix. I've got this wired directly into our post-deploy pipeline — it runs automatically after every production push and blocks the "deploy successful" Slack notification until it passes. Takes about 85-90 seconds end-to-end and has saved us at least four times this year alone.

The trick is testing from the outside in, just like a real user would. Don't hit your origin directly, don't trust the CDN's health check endpoint. Hit the edge via public DNS and validate what actually comes back. I check six things: HTTP status, cache-status or x-cache header, age header sanity, TLS certificate validity, a content hash match for a known canary file, and TTFB under 350ms from at least 3 geographies. If any of those fail, the deploy is marked as "needs investigation" even if the CI job was green.

This is a stripped-down version of what we run. You'll want to customize the URLs and expected hashes, but the logic is solid. It fails fast and tells you exactly which PoP or check flopped. Once you've got this stable, you can graduate to more intense validation like Distributed Load Testing for CDN Edge: Generating Traffic from Multiple Geographies to see how that fresh cache behaves under real pressure.

#!/bin/bash
set -e
CANARY_URL="https://www.example.com/__canary.txt"
EXPECTED_HASH="a3f5c9e8b7d2f1a0c4e9b8a7d6f5e4c3"
EDGE_POPS=("https://www.example.com" "https://eu.example.com" "https://ap.example.com")
ALERT_THRESHOLD_MS=350

echo "Running CDN smoke test..."

# 1. Check canary file hash and cache headers
for POP in "${EDGE_POPS[@]}"; do
  echo "Checking $POP..."
  RESP=$(curl -s -D - -o /tmp/body.txt --connect-timeout 5 --max-time 10 "$POP/__canary.txt")
  STATUS=$(echo "$RESP" | head -n1 | awk '{print $2}')
  CACHE=$(echo "$RESP" | grep -i "^cache-status:\|^x-cache:" | tr -d '\r')
  TTFB=$(curl -s -w "%{time_starttransfer}\n" -o /dev/null --max-time 10 "$POP/__canary.txt")
  TTFB_MS=$(awk "BEGIN {print int($TTFB*1000)}")

  if [ "$STATUS" != "200" ]; then echo "FAIL: $POP returned $STATUS"; exit 1; fi
  if ! echo "$CACHE" | grep -qi "HIT\|MISS"; then echo "FAIL: $POP missing cache header: $CACHE"; exit 1; fi
  if [ "$TTFB_MS" -gt "$ALERT_THRESHOLD_MS" ]; then echo "WARN: $POP TTFB ${TTFB_MS}ms > ${ALERT_THRESHOLD_MS}ms"; fi

  ACTUAL_HASH=$(md5sum /tmp/body.txt | awk '{print $1}')
  if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then echo "FAIL: Hash mismatch at $POP"; exit 1; fi
  echo "PASS: $POP - $CACHE - ${TTFB_MS}ms"
done

# 2. Check stale-while-revalidate and Age header sanity
AGE=$(curl -s -I --max-time 5 "$CANARY_URL" | grep -i "^age:" | awk '{print $2}' | tr -d '\r')
if [ -n "$AGE" ] && [ "$AGE" -gt 86400 ]; then echo "FAIL: Age header suspiciously high: $AGE"; exit 1; fi

echo "All smoke checks passed."

When Your Smoke Test Lies: Troubleshooting False Passes and Ghost Caches

The most dangerous result isn't a fail — it's a false pass. I've seen smoke tests return 200 OK for 20 minutes after a bad purge because they were hitting a single PoP in Ashburn that happened to have the new file, while Singapore and Frankfurt were still serving a 3-hour-old stale copy. If you're only testing one region, you're not testing your CDN. You need to force requests through at least 3-4 geographically dispersed edges, and you need to bypass anycast luck by resolving specific PoP hostnames or using a checker like Catchpoint or ThousandEyes if you have it. Even a simple trick — adding ?smoke=timestamp and then checking without the query string — can expose ghost caches.

Another sneaky one: your cache-control headers look right but your origin is still sending set-cookie or vary: * on static assets, which tells most CDNs "don't cache this at all." Your smoke test will show MISS every time and you’ll think it’s just cold cache, when actually your hit ratio has cratered from 94% to 12% and your origin is about to get hammered. If you see consistent MISS on content that should be HIT after two requests within 30 seconds, dump the full response headers and look for uncacheable flags. I also watch for TTFB variance — if your p50 is 45ms but p95 jumps to 1.2 seconds, you've probably got origin shielding or tiered cache misconfigured.

When a smoke test does fail, don't just roll back immediately. First, isolate: is it cache, config, or origin? Curl the origin directly (bypass CDN with /etc/hosts). If origin is fine, purge one test file and re-request. If that fixes it, you've got a purge propagation issue, not a bad deploy. For deeper resilience work — what happens when an entire PoP goes dark mid-deploy — you’ll want to level up to Chaos Testing at CDN Edge: Injecting Faults to Validate Resilience and keep a CDN Failover Testing Playbook: Scripted Scenarios for Disaster Recovery Validation handy. Smoke tests tell you *if* it's broken; those tell you *how* it breaks.

Frequently Asked Questions

How often should I run CDN smoke tests — just after deploys?

After every deploy is the minimum, but I also run them on a 5-minute cron from an external monitor. Config changes, WAF rule updates, and even expired certificates can break your CDN without a code deploy. That cron has caught expired intermediate certs and accidental cache-rule deletions that would've been invisible until traffic spiked. Keep the scheduled run lightweight — 3 URLs, 3 PoPs — so it doesn't add cost.

My smoke test shows HIT but users still report stale content. What's happening?

You're likely hitting a different cache tier than they are. Most CDNs have edge, shield, and sometimes browser cache layers. A HIT at the edge doesn't mean it's fresh — check the Age header. If Age is 42,000 seconds (almost 12 hours) and you just deployed, your purge didn't propagate or your surrogate keys are wrong. Also verify you're not caching HTML with a 1-day TTL and serving it with stale-while-revalidate that hides the staleness from your checker.

What's a realistic pass criteria for TTFB and hit ratio right after a deploy?

Don't expect a 95% hit ratio immediately after a full purge — you'll be at 40-60% for the first 2-3 minutes while cache warms. I consider it a pass if TTFB is under 350ms at p75 and hit ratio climbs above 85% within 5 minutes for static assets. For dynamic content with short TTLs (like 60s), a 30-45% hit ratio is normal. Track it over 10 minutes, not 10 seconds, or you'll panic over cold cache.

Should I purge everything or use surrogate keys for smoke testing?

Never do a full purge to test a small change if you can avoid it — it nukes your hit ratio and origin. Use surrogate keys or URL-based purging and include a canary file with a unique key (like canary-v123) that you purge and validate. Full purges are for major version bumps only. If your smoke test requires a full purge to pass, your cache key strategy needs work, not your purge button.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker