CloudflareWorkersEdge

Cloudflare Workers + ApogeoAPI: Country Detection at the Edge in 30 Lines

ApogeoAPI6 min read

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