Your CI is green. The deploy finished. Uptime is still 100%.
And your users might still be staring at a blank page.
CI tells you whether your build completed. It does not tell you whether production is serving the right content, with the right assets, from the right place.
The failures that hurt most are rarely total outages. They're partial, silent, and easy to miss if all you track is status codes and deploy success.
The problem with green dashboards
A green deploy only proves your pipeline finished. It doesn't prove users can load your scripts, follow your redirects, or complete a flow. Production can be broken while every dashboard stays green.
Here's what to actually verify — before your users find the problem for you.
1. Verify your asset hashes resolve
After a deploy, your HTML references new hashed filenames — main.c8d13.js, styles.7fb2e.css. If your CDN is still serving the old HTML at some edge locations, those references point to files that no longer exist.
The real problem is the mismatch between the HTML document and the assets it expects — and this is one of the most common silent failures after deploys.
What to check:
# Fetch your page and extract script/stylesheet URLs
curl -s https://yoursite.com | grep -oP '(src|href)="[^"]*\.(js|css)"'
# Then verify each asset returns 200
curl -I https://yoursite.com/_next/static/chunks/main.c8d13.js
Check both the homepage and one or two critical routes — they can reference different bundles. You're looking for 200 (not 404) and a fast response (not a timeout suggesting the file is missing behind a fallback).
When this matters most: Platforms with CDN edge caching (Netlify, Vercel, Cloudflare Pages), especially when your HTML cache TTL outlives your deploy frequency.
2. Check MIME types on your JS and CSS
A JavaScript file served with Content-Type: text/html will be silently blocked by the browser. Your page loads. Your scripts don't execute. No visible error unless you inspect the network tab.
What to check:
curl -I https://yoursite.com/assets/app.js | grep content-type
# Should be: content-type: application/javascript
# Not: content-type: text/html
Do this for your main JS bundle and your primary CSS file at minimum.
When this matters most: Sites behind Nginx with try_files directives that fall back to index.html for everything, or CDNs that replace 404s with branded HTML error pages.
3. Follow your redirect chains
Don't just check that your URL responds — check where it ends up.
What to check:
curl -sIL https://yoursite.com/important-page | grep -E "(HTTP/|location:)"
Look for:
- More than 3 redirects (excessive)
- A loop (same URL appearing twice)
- Landing on an unexpected destination (generic error page, wrong subdomain)
- Final status is not
200 - Protocol normalization fighting itself (HTTP → HTTPS → HTTP)
When this matters most: After changing DNS, CDN rules, SSL configuration, or adding plugins/middleware that modify routing. Especially common when redirect rules at different layers (CDN, origin, application) don't agree with each other.
4. Verify from a different region
Your site works from your location. That doesn't mean it works everywhere.
CDN cache invalidation failures are regional — your local edge got the purge, Frankfurt didn't. Your users in Europe see stale HTML referencing deleted assets while your dashboard says green.
What to check:
The real test is comparing actual responses from multiple locations. If you have a cloud instance in another region, run the same checks from there. A VPN works too.
Compare across regions:
- Does the HTML reference the same asset hashes?
- Do the same assets return
200? - Do redirect chains land on the same destination?
- Do cache headers (
age,x-cache,cf-cache-status) differ?
# Quick DNS sanity check from your machine
dig +short yoursite.com
# Useful for catching post-migration drift, but not a substitute
# for actually fetching content from multiple locations
When this matters most: After DNS changes, CDN migrations, cache purges, and any deploy to a platform with edge caching.
5. Verify the response contains actual content
A page can return 200 OK with valid HTML and still be functionally empty — an app shell with no rendered content because the JavaScript that populates it never loaded.
What to check:
Look for a stable marker that only appears when the page rendered correctly — a page title, a heading, a product name, a form label, or CTA text:
# Check for expected page title
curl -s https://yoursite.com | grep "<title>Your App Name"
# Check for a content marker that should always exist
curl -s https://yoursite.com | grep "Sign in"
# Check the canonical tag as a secondary signal
curl -s https://yoursite.com | grep 'rel="canonical"'
If the response is just a <div id="app"></div> and some <script> tags with none of your expected markers, your framework didn't render. Don't just check the homepage — verify one or two critical routes (checkout, login, pricing) as well.
When this matters most: Any SPA or SSR framework (Next.js, Nuxt, Gatsby) where the HTML is a shell and JavaScript does the rendering. Also useful for catching branded CDN error pages that return 200 with valid HTML — but not your HTML.
6. Check your critical-path dependencies
Your site is fine. Your payment provider's JS CDN is down. Your checkout is broken and you have no idea.
Not every external script matters. Focus on the ones where failure means broken functionality, not just a missing pixel:
- Payment SDK (Stripe, PayPal) — checkout dead
- Auth provider (Auth0, Firebase Auth) — login broken
- Bot protection (reCAPTCHA, Turnstile) — forms unsubmittable
- Feature flags / config endpoint — if your app boot depends on it, it's critical path
What to check:
# Extract external script sources
curl -s https://yoursite.com | grep -oP 'src="https://[^"]*\.js"'
# Verify each critical one responds with correct type
curl -I https://js.stripe.com/v3/
When this matters most: Always. Third-party failures happen independently of your deploys and are outside your control. But they break your users' experience just the same.
7. Compare content fingerprints
The most subtle failures are when your site returns different content than expected — a stale cached version, a CDN error page that returns 200, or content from a completely different environment.
What to check:
# Generate a hash of your page content
curl -s https://yoursite.com | sha256sum
# Compare against a stored baseline
# If the hash changed and you didn't deploy, something is wrong
Store the hash after a known-good deploy and compare on every subsequent check. Update your baseline after intentional deploys — otherwise every deploy looks like a content change. Combine this with the content markers from step 5 to distinguish between "content changed because we deployed" and "content changed because something broke."
When this matters most: Between deploys. If the hash changes and nobody deployed, something is wrong.
A starting point script
Here's a deliberately minimal version. In practice, you'll want route-specific checks, dynamic asset discovery, content markers per route, and multi-region validation. But this covers the basics as a CI post-deploy gate:
#!/bin/bash
SITE="https://yoursite.com"
FAILED=0
echo "Running post-deploy checks for $SITE"
# Check main page responds
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SITE")
if [ "$STATUS" != "200" ]; then
echo "FAIL: Main page returned $STATUS"
FAILED=1
fi
# Check JS bundle MIME type
MIME=$(curl -sI "$SITE/assets/app.js" | grep -i content-type | tr -d '\r')
if [[ "$MIME" != *"javascript"* ]]; then
echo "FAIL: JS bundle MIME type is $MIME"
FAILED=1
fi
# Check redirect chain isn't excessive
REDIRECTS=$(curl -sIL "$SITE" | grep -c "HTTP/")
if [ "$REDIRECTS" -gt 4 ]; then
echo "WARN: $REDIRECTS redirects detected"
fi
# Check for expected content marker
if ! curl -s "$SITE" | grep -q "<title>Your App Name"; then
echo "FAIL: Expected content marker not found — page may not have rendered"
FAILED=1
fi
if [ "$FAILED" -eq 0 ]; then
echo "All checks passed"
else
echo "Issues detected — investigate before closing the deploy"
exit 1
fi
The problem with scripts
A shell script is a good start. It catches the obvious stuff on deploy day.
The problem is what happens after deploy day. Scripts run once. They check from one location. They break when your asset paths change. They don't catch the CDN edge that starts serving stale content three hours later, or the third-party SDK that goes down on Tuesday.
Status codes are a terrible proxy for user experience. The real gap isn't "did this deploy work" — it's "is this site still working, right now, from everywhere."
That's what we built Sitewatch for — continuous verification of everything in this checklist, from multiple regions, on every check cycle. Not just after deploys.
What does your post-deploy verification look like? If you've got a check I didn't cover, drop it in the comments — always looking to make the checklist more complete.
Top comments (0)