Redirects Not Working After Deploy

You configured redirects but they don't fire — order, format, host quirks.

You added a /old-path/new-path 301 in vercel.json / _redirects / netlify.toml, deployed, opened the browser, and either get a 404 or still see the old page. This is the most common “config looks right but isn’t running” failure on web hosts. Usually it’s because the rule file wasn’t included in the build output, an earlier catch-all rule swallowed the path, or the CDN / browser is still serving the cached old response.

This article splits it into 5 causes ordered by hit rate, with curl -I checks you can run for each.

Common causes

Ordered by hit rate, highest first.

1. The rule file isn’t in the build output

vercel.json must be at the repo root. Netlify’s _redirects belongs in public/ (for Astro / Vite) so the build copies it to dist/_redirects. Cloudflare Pages also requires _redirects to end up in the published directory. If you stashed it in src/ or app/, the build won’t copy it.

How to spot it: After npm run build, run ls dist/ or ls .vercel/output/ and confirm _redirects or config.json is really there. On Vercel you can also open a deployment → “Source” tab and search the filename.

2. An earlier catch-all rule wins

Most redirect engines are top-down, first-match-wins. If your vercel.json has:

{
  "redirects": [
    { "source": "/:path*", "destination": "/new-site/:path*", "permanent": true },
    { "source": "/old", "destination": "/new", "permanent": true }
  ]
}

the second rule can never fire because the first consumes everything.

How to spot it: Move /old to the top of the list temporarily. If it suddenly works, it’s an ordering problem.

3. CDN or browser is serving the cached old response

If the previous deploy didn’t have a redirect and the CDN cached a 200 + HTML for that URL, the new rule won’t take effect until the cache expires. Cloudflare defaults to a 4-hour edge cache on HTML; Vercel Edge also caches redirect responses themselves.

How to spot it: Run curl -I "https://yourdomain.com/old-path?cb=$(date +%s)" with a cache-buster query. If with the buster you see 301 but without you see 200, it’s caching.

4. Trailing slash mismatch

Most platforms treat /old and /old/ as different URLs. Vercel defaults to trailingSlash: false, so a rule written as /old/ will never match a visitor hitting /old. Same trap on Netlify _redirects.

How to spot it: Normalize the slash in your source rule to match your site’s actual URL style and retest.

5. You used rewrites when you wanted redirects

rewrites keep the URL in the address bar and proxy internally; the user still sees /old. redirects actually send the browser to /new with a 301/302. Mixing up the two fields gives you the “looks like it jumped but didn’t” symptom.

How to spot it: curl -I and read the status code — 200 means rewrite, 301/302/308 means redirect.

Shortest path to fix

Ordered by ROI. The first three usually solve 80% of cases.

Step 1: Use curl to see the real response

Don’t trust the address bar — the browser auto-follows redirects and may hit local cache. Curl directly:

curl -I -L "https://yourdomain.com/old-path?cb=$(date +%s)"

Read the output:

  • First response is 301 Moved Permanently + location: /new-path → redirect works
  • First response is 200 → rule didn’t fire (see Step 2)
  • First response is 308 and location points back to the same path with a slash → that’s the platform’s trailing-slash behavior, not your rule
  • cf-cache-status: HIT / x-vercel-cache: HIT → caching (see Step 3)

Step 2: Confirm the file is in the artifact and the order is right

Build locally and inspect:

npm run build
ls -la dist/ | grep -E '_redirects|vercel.json'
cat dist/_redirects 2>/dev/null || cat vercel.json

If the file isn’t in dist/, move it to where the platform expects:

PlatformRule fileMust live in
Vercelvercel.jsonRepo root
Netlify_redirects or netlify.tomlpublic/ or root
Cloudflare Pages_redirectsPublished directory (often public/ or dist/)
Astro static_redirectspublic/_redirects

Then put specific rules before generic ones. Vercel example:

{
  "redirects": [
    { "source": "/old", "destination": "/new", "permanent": true },
    { "source": "/blog/:slug", "destination": "/articles/:slug", "permanent": true },
    { "source": "/legacy/:path*", "destination": "/", "permanent": false }
  ]
}

Step 3: Purge the CDN for that one URL

Don’t purge everything — expensive and rarely needed. Purge just the affected URL:

  • Cloudflare: Caching → Configuration → Purge Custom URLs, paste the full URL (with https://)
  • Vercel: Redeploy, or run vercel --prod --force for the project
  • Netlify: Deploys → latest deploy → Trigger deploy → Clear cache and deploy site

Re-run Step 1’s curl with a cache buster to confirm.

Step 4: Add a CI smoke test

The cheapest regression guard is a post-deploy hook that curls a handful of redirects and asserts the status and location. Example:

#!/usr/bin/env bash
set -e
BASE="https://yourdomain.com"
check() {
  local from="$1" expected="$2"
  local actual=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "$BASE$from")
  if [[ "$actual" != "$expected"* ]]; then
    echo "FAIL $from → got '$actual', expected '$expected'"; exit 1
  fi
}
check "/old" "301 ${BASE}/new"
check "/blog/hello" "301 ${BASE}/articles/hello"
echo "All redirects OK"

Run it on the GitHub Actions deployment_status event.

Prevention

  • Verify with curl -I right after you ship a redirect, never trust the browser alone
  • Order rules: specific first, catch-all last; add comments to mark the boundary
  • Run a 5-10 URL smoke test post-deploy in CI
  • Pick one trailing-slash policy site-wide and make rule sources match
  • Commit before bulk redirect edits so you can revert by file diff

Tags: #Hosting #Debug #Troubleshooting