You have a multi-region site: example.com/us, example.com/uk, example.com/de. When a user from Germany lands on example.com, you want them to see the German version. The standard solution: geo-redirect, where you detect the user’s country from their IP and redirect to the regional URL.
This sounds simple. It’s not. Done poorly, geo-redirects hurt SEO, frustrate users who want a different region, and confuse caches. Done well, they’re invisible.
This post covers the patterns that actually work in 2026.
Why You Might Want Geo-Redirects
- Language localization — Show users content in their language.
- Currency / pricing — Show local currency and tax-included prices.
- Regulatory compliance — Different terms / privacy notices per region.
- Inventory / availability — Products that vary by region.
- Content licensing — Show users content licensed in their country.
For some of these (language) a popup banner is better than a redirect. For others (currency, inventory) a redirect makes more sense.
The Simple (Wrong) Implementation
app.get('/', async (req, res) => {
const country = await getCountry(req.ip)
if (country === 'DE') return res.redirect('/de')
if (country === 'FR') return res.redirect('/fr')
return res.redirect('/us') // default
})
This is the naive version. It “works” but has many problems:
- Search engines (Googlebot) get redirected based on the IP they’re crawling from — usually US. They never index
/deor/fr. - Users who want a different region can’t easily access it.
- Returning users always get redirected, even after they manually chose another region.
- Caching breaks — the cached version depends on the user’s IP.
Each problem has a fix; together they make geo-redirect more complex than it looks.
Fix 1: Honor User Choice
Once a user picks a region, remember it. Don’t keep redirecting them.
app.get('/', async (req, res) => {
// Check cookie first
const preferredRegion = req.cookies.region
if (preferredRegion) {
return res.redirect(`/${preferredRegion}`)
}
// Fall back to IP-based
const country = await getCountry(req.ip)
const region = countryToRegion(country)
return res.redirect(`/${region}`)
})
When the user explicitly visits /de, set the cookie:
app.get('/de', (req, res, next) => {
res.cookie('region', 'de', { maxAge: 365 * 24 * 60 * 60 * 1000 })
next()
})
Now repeat visits remember the choice. A user from Germany who manually clicks “View US site” doesn’t keep getting redirected to /de.
Fix 2: Search Engine Handling
Search engines crawl from various IPs but want to index each regional version. Two approaches:
Option A: Don’t redirect crawlers
Detect search engine bots via User-Agent + verified reverse DNS (see bot detection). Serve content directly without redirect.
async function isVerifiedSearchBot(req: Request): Promise<boolean> {
const ua = req.headers['user-agent'] ?? ''
if (!/Googlebot|Bingbot/i.test(ua)) return false
// Verify via reverse DNS
const reverseDns = await lookupPtr(req.ip)
return reverseDns.endsWith('.googlebot.com') || reverseDns.endsWith('.googlebot.com.')
}
app.get('/', async (req, res) => {
if (await isVerifiedSearchBot(req)) {
return serveDefaultContent(req, res)
}
// ... normal geo logic
})
Option B: Use hreflang
Tell search engines about regional versions via <link rel="alternate" hreflang>:
<link rel="alternate" hreflang="en-US" href="https://example.com/us" />
<link rel="alternate" hreflang="de-DE" href="https://example.com/de" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
Each regional page includes hreflang for all variants. Search engines understand the relationship and index each version. With this in place, geo-redirects for normal users don’t hurt SEO.
Use both: hreflang for the global picture, and don’t redirect crawlers to avoid the situation where Googlebot can’t see your regional content.
Fix 3: Caching Considerations
If you cache / aggressively, you can’t serve different content per region. Options:
Don’t cache /
Make it dynamic; let the geo-redirect run for every request. Simple but defeats the cache.
Cache per-region
Use Vary or different cache keys per region. The CDN serves the cached redirect for each region.
Cache-Control: public, max-age=300
Vary: X-Country
Some CDNs (Cloudflare, Fastly) let you set cache keys based on geo headers.
Edge-side redirects
Do the redirect at the CDN/edge layer instead of origin. Cloudflare Workers, Vercel Edge Functions, AWS Lambda@Edge can all do this without hitting origin.
// Cloudflare Worker
export default {
async fetch(request: Request) {
const url = new URL(request.url)
if (url.pathname === '/') {
const country = request.headers.get('cf-ipcountry')
const region = countryToRegion(country)
return Response.redirect(`${url.origin}/${region}`, 302)
}
return fetch(request)
}
}
This is fast and avoids origin requests entirely for redirects.
Fix 4: 302 vs 301
A subtle but important choice:
- 301 Permanent Redirect — Caches forever. Search engines treat the destination as the canonical URL.
- 302 Temporary Redirect — Doesn’t cache forever. Each request hits your server.
For geo-redirects: use 302. The redirect is context-dependent (changes per user); 301 would have terrible side effects (search engines indexing the wrong URL for the canonical, browsers caching the redirect indefinitely).
A Complete Pattern
Putting it together:
async function handleRoot(req: Request, res: Response) {
// 1. Search engine: serve content directly with hreflang
if (await isVerifiedSearchBot(req)) {
return serveDefaultWithHreflang(req, res)
}
// 2. Honor user's explicit choice from cookie
const cookieRegion = req.cookies?.region
if (cookieRegion && SUPPORTED_REGIONS.has(cookieRegion)) {
return res.redirect(302, `/${cookieRegion}`)
}
// 3. Geo-detect by IP
const country = req.headers['cf-ipcountry'] ?? await getCountry(req.ip)
const region = countryToRegion(country) ?? 'us' // default to /us
// 4. Redirect (302 since geo is context-dependent)
return res.redirect(302, `/${region}`)
}
// Set region cookie when user explicitly visits a region
app.get('/:region/*', (req, res, next) => {
if (SUPPORTED_REGIONS.has(req.params.region)) {
res.cookie('region', req.params.region, {
maxAge: 365 * 24 * 60 * 60 * 1000,
sameSite: 'lax'
})
}
next()
})
This handles:
- Crawlers correctly.
- Returning users with explicit preferences.
- New users via geo.
- Sensible defaults.
UX Considerations
A few things to get right beyond the technical:
Show a region switcher
Even with auto-detection, every page should have a clear “switch region” UI. Users in Germany on business travel may want the US version.
Don’t be aggressive
Some sites force users back to the auto-detected region. Frustrating for legitimate use cases. Users who explicitly clicked a region should stay there.
Announce the redirect (sometimes)
A banner like “We detected you’re in Germany. View our German site?” is friendlier than silent redirect for the first visit.
Mobile considerations
Mobile users on roaming may have IPs from other countries. See mobile network architecture. Your auto-detected region might be wrong; the switcher is essential.
VPN users
A VPN user is intentionally in a different region. Respect their choice; don’t fight them.
Hreflang in Depth
A few details for hreflang:
- Each region’s page should list all variants including itself.
- Use ISO 639-1 + ISO 3166-1 codes:
en-US,de-DE,pt-BR. x-defaultis the global / no-specific-region fallback.- Bidirectional —
/ussays it has/de;/desays it has/us. Both directions for search engines to validate.
This is a separate (but related) SEO concern from geo-redirect logic. Both should be in place.
Geo-Restriction (Hard Block) vs Geo-Redirect
A note on the difference:
- Geo-redirect — User goes to the right page automatically. Friendly. Reversible.
- Geo-restriction — User is blocked entirely from some regions. Hard. See geofencing 101.
Streaming services often use restriction (license-driven). Most other use cases want redirect, not restriction.
Using the IP API
For detecting country, you can use:
- CDN headers (
CF-IPCountry,CloudFront-Viewer-Country) if you’re behind a CDN. Zero-latency, no API call. - Hosted API like Ip2Geo if you’re not behind a CDN or want richer data.
- Local database (GeoLite2 or commercial) for low-latency on-premise.
For details, see detect country from IP.
TL;DR
- Geo-redirect sends users to a regional URL based on IP-derived country.
- Use 302, not 301. Geo is context-dependent.
- Honor user cookies for explicit region choices.
- Handle search engines separately (verified bot detection + hreflang).
- Implement region switcher UI for users who need to override.
- Edge-side redirects (Cloudflare Workers, etc.) are fast and cache-friendly.
- hreflang tags help search engines index each regional version.
- VPN users and roaming users make IP-based geo imperfect — the switcher fixes this.
Geo-redirects look like a simple “if country = X, redirect to Y” but production-quality implementations handle multiple concerns (SEO, returning users, caches, user choice). For a deeper look at geographic personalization patterns, see geo personalization; for hard regional access controls, geofencing 101.