301 vs 302 Decision Trees for Technical Site Migrations

Context

Selecting the correct HTTP status code dictates crawler behaviour, cache control, and link equity transfer. A 301 (Moved Permanently) instructs search engines to replace the old index entry and transfer ranking signals to the new URL. A 302 (Found) signals a temporary route, preserving the original URL’s indexing and authority — the search engine should keep ranking the source URL and treat the destination as a stopgap. Misalignment fragments index topology, delays equity consolidation, and produces conflicting canonical signals that can persist for months.

This decision happens at the point in the migration timeline where you convert a finalised mapping inventory into deployable rules. Webmasters and SEO engineers hit it most often when a migration overlaps with seasonal campaigns, staged rollouts, or maintenance windows, where the temptation to “use 302 just in case” quietly stalls index consolidation. Align your status code selection with the broader URL Mapping & Redirect Architecture playbook to maintain consistent routing across edge and origin layers, and verify the destinations against the routing changes introduced by your platform under CMS & Framework Routing Changes.

The cost of getting this wrong is asymmetric and delayed, which is what makes it dangerous. A 302 on a permanent move does not throw an error or break the page — the user lands where you intended — so the defect is invisible to functional QA and surfaces only weeks later as flat or declining rankings on the destination while the retired source clings to its index entry. By then the cache has propagated, the migration has been signed off, and unwinding the mistake means re-crawling and re-consolidating from scratch. Treating the status code as a deliberate, reviewed decision per route, rather than a default someone picks at deploy time, is the cheapest insurance available on a migration. The decision tree in this playbook reduces that choice to three questions — is the move permanent, is it part of a phased rollout, and does the route carry a request body — answered in that order.

301 versus 302 decision tree A decision tree routing a redirect to 301, 302, or 308 based on permanence, request method, and rollout phase. Redirect Status Decision Tree Is the move permanent? content stays at new URL No Yes Phased or A/B rollout? temporary detour Preserve POST method? API or form endpoint 302 Found 301 Moved 308 Perm.
Permanence, rollout phase, and request method drive the choice between 301, 302, and 308.

Pre-flight Checks

  • Lower legacy domain DNS TTL to 300 s at least 48 hours before cutover so corrected rules propagate quickly.
  • Maintain active A/CNAME records for the legacy domain throughout the migration window.
  • Audit existing internal canonical tags and hreflang attributes for conflicts with the planned status codes.
  • Verify zero application-layer redirects are intercepting traffic before server-level rules execute.
  • Prepare a clean legacy-to-destination URL inventory with strict 1:1 matching, ideally exported from CSV Mapping Workflows.
  • Validate that all destination URLs return 200 OK before mapping any source to them.
  • Confirm CDN edge configurations allow custom Cache-Control and status-code overrides.
  • Snapshot the current routing config in Git so a rollback target exists before the first change.

Execution Steps

1. Classify Each Route by Permanence

Evaluate content permanence before assigning any status code. Use 301 for permanent consolidation, URL cleanup, domain changes, or protocol upgrades (HTTP → HTTPS). Use 302 for A/B testing, seasonal routing, temporary staging handoffs, or maintenance-window detours where the source URL must retain its ranking. Tag every row in the mapping inventory with its classification so the rule generator can emit the correct code deterministically rather than relying on a per-engineer judgement call at deploy time.

The single most expensive mistake at this step is treating “I am not sure yet” as a reason to reach for 302. A 302 tells search engines to keep the old URL indexed and discount the destination, so a permanent move dressed up as temporary leaves ranking signals stranded on a URL you intend to retire. The inverse error — a 301 on a genuinely temporary route — bakes the detour into browser and CDN caches for days or months, making it painful to reverse. Decide permanence explicitly per route and record the reason in the inventory, because that note is what a reviewer reads when a status code is later questioned. For any route you cannot confidently classify, default to keeping the source live and the destination behind a 302 until parity is proven, then promote.

2. Branch Phased Rollouts to 302

Map phased rollout triggers to staging validation, traffic shifting, and canonical handoff milestones. While a destination is still provisional — partial traffic, unverified content parity, or an active experiment — keep it on a 302 so search engines continue to rank the stable source URL. For incremental transitions, follow When to Use 302 Redirects During Phased Migrations to manage temporary routing without fragmenting crawl budgets, then promote the route to 301 once parity is confirmed.

The promotion from 302 to 301 is itself a milestone, not an afterthought. Define a concrete gate — for example, destination parity verified, error rate under the rollback threshold for 72 hours, and Search Console showing the destination crawled — and flip the status code in one controlled deploy across the affected segment. Promoting routes piecemeal leaves a mixed 301/302 estate that is hard to reason about during an incident. Keep Cache-Control short on the 302 phase so the promotion propagates quickly once you commit to it.

3. Reserve 308 for Method-Preserving Routes

A 301 permits user agents to downgrade a POST to a GET on the follow-up request; a 308 (Permanent Redirect) forbids that and replays the original method and body. For form submission endpoints, webhooks, and APIs that move during a migration, choose 308 to avoid silently dropping payloads. Walk the trade-offs in Choosing 308 vs 301 for Method-Preserving Redirects before applying it to public, cacheable HTML routes where 301 remains the safer, better-supported default.

The mirror of 308 is 307 (Temporary Redirect), which preserves the method like 308 but signals temporariness like 302. The full matrix is therefore two axes: permanence (temporary vs permanent) and method handling (may downgrade vs must preserve). Map each moving endpoint onto that grid — a public article uses 301, a moving form POST uses 308, a temporarily relocated API in a staged cutover uses 307 — and never assume client and intermediary support for 307/308 is universal; older proxies and a handful of bots still mishandle them, so reserve the method-preserving codes for routes that genuinely carry a request body.

4. Generate Rules from the Inventory

Structure your redirect inventory to prevent soft 404s, and process enterprise-scale mappings through automated pipelines rather than hand-editing config. Standardise data preparation and pre-deployment QA using CSV Mapping Workflows to ensure accurate bulk imports and version control. Map legacy 404/410 endpoints to category-level fallbacks instead of the homepage so the topical signal of the original URL is not collapsed into a soft 404.

Generating rules from a single inventory file rather than editing server config directly is what makes the status-code policy auditable. Each row carries its source, destination, and chosen code, so a diff of the inventory is a diff of the routing behaviour — far easier to review than scattered RewriteRule lines. The generator should refuse to emit a 301 whose destination does not already return 200, and refuse to emit any rule whose source also appears as another rule’s destination, catching equity-leaking chains before they ship. Treat the inventory as the source of truth and the compiled config as a build artefact you never hand-edit.

5. Deploy at the Edge or Web Server

Deploy rules at the web server or CDN edge, never the application layer where framework boot adds latency. Prioritise return 301 over rewrite in Nginx to minimise processing cost, and apply Apache RedirectMatch or mod_rewrite with strict anchor boundaries. Handle legacy path transformations and capture groups safely by referencing Regex Redirect Rules to prevent backtracking and route collisions.

The layer you choose also dictates how the status code is cached and reversed. Edge redirects respond before the request ever reaches the origin, which is fastest and cheapest but means a wrong code is cached across every edge node and must be purged globally to undo. Web-server redirects keep control closer to the application and are simpler to revert via a config rollback, at the cost of an origin round-trip. Whichever layer owns the rule, make sure it is the only layer issuing a redirect for that path — a status code set at the edge and re-issued at the origin is the most common way a clean 301 silently becomes a two-hop chain.

6. Flatten Before You Ship

A correct status code on a chained route still wastes crawl budget. Before promoting anything to production, run the route set through Redirect Chain Elimination so every source resolves to its final destination in a single hop. A 301 that lands on another 301 leaks ranking signals and adds round-trips that degrade Time to First Byte.

Status-code correctness and chain flatness are independent properties, and you need both. A route can be a perfect 301 and still pass through two intermediate hops because an older rule from a previous migration sits in the path; conversely, a single-hop route can carry the wrong code. Resolve the chain in the inventory data first so the emitted rule points at the terminal canonical URL, then verify hop count at runtime with curl. Re-run this flattening check after every config change, since adding one new pattern can quietly re-chain routes that were previously direct.

Configs / Commands

Nginx — permanent domain redirect (preserves path and query):

# return is cheaper than rewrite; $request_uri keeps path + query string
server {
    listen 80;
    server_name legacy-domain.com;
    return 301 https://target-domain.com$request_uri;
}

Apache — permanent domain redirect with mod_rewrite:

RewriteEngine On
# Match the legacy host, then 301 every path to the new domain
RewriteCond %{HTTP_HOST} ^legacy-domain\.com$ [NC]
RewriteRule ^(.*)$ https://target-domain.com/$1 [R=301,L]

Apache — 308 for a method-preserving form endpoint:

# R=308 replays POST body to the new endpoint instead of downgrading to GET
RewriteRule ^api/submit$ https://target-domain.com/api/v2/submit [R=308,L]

Cloudflare Redirect Rule (Redirect Rules UI / Ruleset API):

# Edge-level 301 keeps origin out of the redirect path entirely
Match: (http.host eq "legacy-domain.com")
Action: Dynamic Redirect -> 301 -> concat("https://target-domain.com", http.request.uri.path)

Note on Cloudflare Page Rules: The legacy Page Rules product is being phased out in favour of Redirect Rules and Transform Rules. Use the Ruleset API or the Redirect Rules UI for new configurations.

Validation

Enforce direct path resolution. Every request must resolve in a single hop (301/302/308 → 200) with the correct code.

# Status code + effective destination in one curl call
curl -sI -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  https://legacy-domain.com/test-path

# Confirm hop count is exactly 1 after following the chain
curl -sI -L -o /dev/null -w 'hops:%{num_redirects} status:%{http_code}\n' \
  https://legacy-domain.com/test-path

# Tally 301/302 responses by path from the access log
grep -E '" 30[12] ' /var/log/nginx/access.log \
  | awk '{print $9, $7}' | sort | uniq -c | sort -nr
  • hops > 1; flatten any detected chain at the origin rule.
  • $request_uri (Nginx) or %{QUERY_STRING} (Apache).

Rollback Triggers

Browser and CDN cache TTLs dictate rollback feasibility. 301 responses cache aggressively (days to months in browsers), while 302s cache only briefly. Plan rollback around these numeric thresholds:

  • Cache-Control: no-cache or max-age=0 during the initial rollout window for critical paths to keep rollback flexible.

After any trigger fires, revert to the previous routing snapshot, purge CDN edge cache, and confirm legacy paths return their pre-deploy responses before attempting a corrected re-deploy.

FAQ

Does Google treat 302 redirects as 301s after a certain period? Google may eventually interpret a persistent 302 as permanent, but this is heuristic and unreliable. Relying on it risks index fragmentation and delayed consolidation. Use 301 explicitly for permanent moves to guarantee predictable equity transfer and canonicalisation.

When should I use a 308 instead of a 301? Use 308 when the redirected route must preserve the original HTTP method and body — form POSTs, webhooks, and API endpoints — because 301 allows clients to downgrade POST to GET. For ordinary cacheable HTML pages, 301 remains the better-supported default.

Should 301 redirects be implemented at the DNS, CDN, or web server level? DNS cannot issue HTTP redirects — it only resolves IPs. Implement 301s at the CDN edge (Cloudflare Redirect Rules, CloudFront Functions) for lowest latency, or at the web server (Nginx/Apache) if CDN routing is unavailable. Avoid application-layer redirects to prevent unnecessary origin processing.

What is the maximum safe number of redirects per URL path? Zero chains. Every request should resolve in a single hop. Browsers typically follow up to 20 hops before aborting, but any chain degrades Core Web Vitals (LCP, TTFB), wastes crawl budget, and increases timeout risk during traffic spikes.

Related

← Back to URL Mapping & Redirect Architecture

Explore Sub-topics