Tutorial · cloudflare · Published 2026-08-16 · 4 min read
Build a redirect rule with Cloudflare Workers
How to redirect legacy paths with a Cloudflare Worker: a small fetch handler, the pattern for prefix and regex redirects, and when a Single Redirect beats it.
When a Worker beats a redirect rule
Cloudflare's built-in redirect options cover the common cases: a Single Redirect matching a path, a Bulk Redirect list for thousands of entries, and a Redirect Rule for query-aware juggling. A Worker is the right tool only when the logic cannot be expressed as a static map:
- The target depends on something you compute per request (geolocation, a cookie, an A/B flag).
- You need to inspect the query string, not just the path, and react with parts of it in the destination.
- Your redirect set is generated by code and changes shape with each deploy, so you want the file to be code, not a CSV.
- You need to stray from a plain
Locationheader (set a cache header before redirecting, or set a cookie).
The default answer for most sites is still the built-in rule. This article is for the long tail where a Worker is genuinely the smallest honest option.
The redirect handler
A Worker redirect is a fetch handler that returns a 301/302 with a Location. The simplest shape:
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/legacy-page") {
return Response.redirect("https://www.example.com/new-page/", 301);
}
return fetch(request);
},
};
new URL(request.url) gives you the pathname, searchParams, host, and protocol of the incoming request. Response.redirect sets the Location header and a status code for you; for a permanent move use 301, for a temporary or A/B test use 302.
A path-table redirect
For a set of exact paths, keep the map in a constant and look it up:
const redirects = {
"/old-about": "/about/",
"/old-contact": "/contact/",
"/old-services/": "/services/",
};
export default {
async fetch(request) {
const url = new URL(request.url);
const target = redirects[url.pathname];
if (target) return Response.redirect(target, 301);
return fetch(request);
},
};
A map entry that does not end in / matches exactly, so appending a trailing slash consistently is still your discipline. The redirect map article explains the SEO side, which states the trailing-slash rule as part of why you standardize.
Redirect when parts of the URL need to carry into the destination
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/search") {
const q = url.searchParams.get("q") || "";
return Response.redirect(`https://www.example.com/s?q=${encodeURIComponent(q)}`, 301);
}
return fetch(request);
},
};
Query strings are preserved only because you forward them explicitly. new URL(request.url) gives you the searchParams; anything you want in the destination must be in the Location you build.
From pattern to deploy
- Set the worker in the dashboard or
wranglerdeploy, and add a route so the worker only runs on the paths you want (routing/*and conditionpathnamein JS is redundant and costs a request per miss, which is wasteful). - Use the Workers playground first: it edits and runs your code before you tie it to production traffic.
- After each deploy, verify with curl from outside:
curl -I https://example.com/oldshould return301 Location: ....
Verification table
| Traffic | Expected Location |
|---|---|
example.com/old | https://www.example.com/about/ with 301 |
example.com/old?q=x | https://www.example.com/about/?q=x with 301 |
example.com/other | passes through to the origin |
When the redirect is one route, prefer a built-in rule over a worker. When it is thousands of routes or dynamic derivation, keep the worker tiny and the map data in a KV or the constant, which is the Workers basics pattern.