Cloudflare Workers + ApogeoAPI: Country Detection at the Edge in 30 Lines
Cloudflare Workers are the most underrated edge runtime in 2026. They run in 300+ cities, cold-start in milliseconds, and the free tier is 100,000 requests / day — generous enough to power a real product.
Pairing Workers with ApogeoAPI gives you geographic data anywhere on Earth, with sub-50ms latency from the user's perspective. Here's the full implementation with KV caching for cost control.
The Worker (30 lines)
// index.ts — deploy with: wrangler deploy
export interface Env {
APOGEOAPI_KEY: string;
GEO_CACHE: KVNamespace; // KV namespace bound in wrangler.toml
}
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise {
const ip = req.headers.get('cf-connecting-ip');
if (!ip) return new Response('No IP', { status: 400 });
// Try KV cache first (free, sub-millisecond)
const cached = await env.GEO_CACHE.get(`geo:${ip}`);
if (cached) return jsonResponse(JSON.parse(cached));
// Fall through to ApogeoAPI
const res = await fetch(`https://api.apogeoapi.com/v1/ip/${ip}`, {
headers: { 'X-API-Key': env.APOGEOAPI_KEY },
});
if (!res.ok) return new Response('Geo lookup failed', { status: 502 });
const data = await res.json();
const out = {
country: data.country.name,
iso2: data.country.iso2,
city: data.city?.name,
timezone: data.timezone?.name,
currency: data.country.currency,
currencyRateUSD: data.country.currencyRate,
};
// Cache 1h in KV (matches typical user session length)
ctx.waitUntil(env.GEO_CACHE.put(`geo:${ip}`, JSON.stringify(out), { expirationTtl: 3600 }));
return jsonResponse(out);
},
};
function jsonResponse(data: unknown): Response {
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'private, max-age=300' },
});
}
wrangler.toml
name = "geo-edge"
main = "index.ts"
compatibility_date = "2026-01-01"
[[kv_namespaces]]
binding = "GEO_CACHE"
id = "your-kv-namespace-id"
[vars]
# APOGEOAPI_KEY set via: wrangler secret put APOGEOAPI_KEY
Deploy
npm install -g wrangler
wrangler kv:namespace create "GEO_CACHE" # paste the id into wrangler.toml
wrangler secret put APOGEOAPI_KEY # paste your key when prompted
wrangler deploy
Two minutes from npm install to live worker.
Why cf-connecting-ip instead of x-forwarded-for?
Cloudflare automatically sets cf-connecting-ip to the visitor's true IP, even when other proxies are in front. x-forwarded-for is spoofable and unreliable. On Cloudflare Workers, always use cf-connecting-ip.
The KV cache decision
Why KV instead of Cache API or in-memory?
- Cache API is per-datacenter — a user routed to a different city on retry hits a cold cache.
- In-memory dies with the worker instance (which may be every few seconds at low traffic).
- KV is globally replicated, sub-millisecond reads, and the free tier covers 100,000 reads / day. For geo-by-IP this matches the access pattern perfectly.
Cost analysis at scale
Suppose you're running this Worker for 1 million visitors / month, with each visitor making 5 requests:
- Worker invocations: 5M / month. Free up to 100K/day = 3M/month. Above that, $0.50 per million.
- KV reads: 5M (one per request, mostly cache hits). Free up to 100K/day = 3M/month. Above that, $0.50 per million.
- KV writes: 1M (one per unique visitor with cold cache). Free up to 1K/day = 30K/month. Above that, $5 per million.
- ApogeoAPI: 1M lookups (one per unique IP, since cache absorbs the rest). Basic plan = $19/mo for 15K. Starter = $29/mo for 100K. Above that, Professional at $79 for 500K.
Worst case at 1M visitors/month: ~$30 in Cloudflare costs + $79 ApogeoAPI = $110/mo all-in for global edge geo-detection. For comparison, MaxMind's commercial GeoIP2 license starts at $370/mo for similar coverage.
Adding currency conversion at the edge
The Worker above already returns currencyRateUSD. Your frontend can use it to localize prices without a second roundtrip:
// On your site:
const geo = await fetch('https://geo-edge.your-domain.workers.dev').then(r => r.json());
const usdPrice = 79;
const localPrice = Math.round(usdPrice * geo.currencyRateUSD);
document.querySelector('#price').textContent =
new Intl.NumberFormat(undefined, { style: 'currency', currency: geo.currency }).format(localPrice);
Useful patterns this enables
- A/B test by region — different copy or pricing per country, decided at the edge.
- Compliance routing — block or redirect visitors from regions you don't serve (no need for a full geo-blocking service).
- Localized signup defaults — pre-fill country dropdowns, currency, language at form load.
- Edge analytics — log country distribution without bloating your main app's request log.
Free ApogeoAPI key + 1,000 calls/month at apogeoapi.com. The Cloudflare Workers free tier covers most starter projects without ever hitting paid limits.
Try ApogeoAPI free
1,000 requests/month forever. 14-day full-access trial. No credit card.
Get your free API key