A request made from one region can see a different result than the same request made from another. That matters for availability checks, regional feature flags, CDN debugging, API smoke tests, and any workflow where the origin of the request is part of the result.
Durable Objects support location hints. A hint tells Cloudflare where you would like a Durable Object instance to be created. That makes a Durable Object useful as a regional HTTP proxy: create one object per region, send requests through it, and let the object make the outbound request.
Create the Durable Object
Use a Durable Object fetch() handler as a small proxy. The outbound fetch() runs inside the Durable Object instance, so it starts from wherever that object is placed.
// app/durable-objects/geo-fetch.tsimport { DurableObject } from "cloudflare:workers"; export interface Env { GEO_FETCH: DurableObjectNamespace<GeoFetch>; } export class GeoFetch extends DurableObject<Env> { override async fetch(request: Request): Promise<Response> { let start = performance.now(); let response = await fetch(request); let end = performance.now(); let headers = new Headers(response.headers); headers.set("x-response-time-ms", String(end - start)); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); } }
This object does not need storage. It exists to pin outbound requests to a Durable Object instance. If you later store request history in the object, write it to Durable Object storage before caching anything in memory.
Configure the Binding
Bind the Durable Object class in wrangler.jsonc. Use a SQLite migration for new Durable Object classes.
// wrangler.jsonc{ "durable_objects": { "bindings": [{ "name": "GEO_FETCH", "class_name": "GeoFetch" }], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["GeoFetch"] }], }
The binding gives your Worker access to the GEO_FETCH namespace. The migration tells Cloudflare this Worker owns the Durable Object class.
Export the Class
Durable Object classes must be exported from the Worker entry point so Cloudflare can instantiate them.
// worker.tsexport { GeoFetch } from "./app/durable-objects/geo-fetch";
Choose a Region
Define the location hints your application supports. Cloudflare treats hints as placement preferences, not hard guarantees.
// app/geo/location-hints.tsexport type LocationHint = | "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me"; export function isEuropeanLocation(locationHint: LocationHint) { return locationHint === "weur" || locationHint === "eeur"; } export function parseLocationHint(value: string | null): LocationHint | null { if ( value === "wnam" || value === "enam" || value === "sam" || value === "weur" || value === "eeur" || value === "apac" || value === "apac-ne" || value === "apac-se" || value === "oc" || value === "afr" || value === "me" ) { return value; } return null; }
The narrower apac-ne and apac-se hints are useful when you care about Northeast or Southeast Asia specifically. Use apac when you only need a broad Asia-Pacific check.
Fetch Through a Regional Object
Create one deterministic Durable Object per location hint. Apply the EU jurisdiction before creating the ID, otherwise the ID and namespace can disagree about jurisdiction.
// app/geo/fetch-from-region.tsimport { env } from "cloudflare:workers"; import { isEuropeanLocation, type LocationHint } from "./location-hints"; export async function fetchFromRegion(url: string, locationHint: LocationHint, init?: RequestInit) { let namespace = isEuropeanLocation(locationHint) ? env.GEO_FETCH.jurisdiction("eu") : env.GEO_FETCH; let id = namespace.idFromName(locationHint); let stub = namespace.get(id, { locationHint }); return await stub.fetch(url, init); }
idFromName(locationHint) makes routing deterministic: every Western Europe request uses the same Western Europe object ID. The locationHint option on get() is only considered when the object is created for the first time.
The EU jurisdiction is different from a location hint. jurisdiction("eu") restricts where the Durable Object runs and stores data. A location hint only asks Cloudflare to place the object near a region.
Expose a Worker Endpoint
Now expose a small endpoint that accepts a target URL and a region. Treat this as an internal endpoint unless you add strict allowlists, otherwise you have built an open proxy.
// worker.tsimport { fetchFromRegion } from "./app/geo/fetch-from-region"; import { parseLocationHint } from "./app/geo/location-hints"; export { GeoFetch } from "./app/durable-objects/geo-fetch"; export default { async fetch(request: Request): Promise<Response> { let url = new URL(request.url); let target = url.searchParams.get("url"); let locationHint = parseLocationHint(url.searchParams.get("region")); if (!target || !locationHint) { return Response.json({ error: "Missing url or region." }, { status: 400 }); } let targetUrl = parseAllowedTarget(target); if (!targetUrl) { return Response.json({ error: "Invalid target URL." }, { status: 400 }); } return await fetchFromRegion(targetUrl.href, locationHint, { method: "GET" }); }, }; function parseAllowedTarget(value: string) { if (!URL.canParse(value)) { return null; } let url = new URL(value); if (url.protocol !== "https:") { return null; } return url; }
The response comes from the target server, with an extra x-response-time-ms header added by the Durable Object. In a real endpoint, expand parseAllowedTarget() to enforce your own domains, customer allowlists, or signed requests.
Understand the Limits
Location hints are best effort. Cloudflare tries to place the Durable Object near the hinted region, but it may choose another nearby region if the exact one is unavailable. Durable Objects also do not move after creation, so changing the hint later does not relocate an existing object.
Some hints are broader than others. For example, apac covers Asia-Pacific, while apac-ne and apac-se target narrower subregions. Some hinted regions may currently spawn in a nearby supported region instead of the named region.
Use this pattern when the request origin matters. Do not use one global Durable Object for every region. One object per region keeps the design simple and avoids turning one instance into a bottleneck.