DEV Community

Cover image for IP geolocation with zero external APIs, the Cloudflare Workers cf object
Koray KÖYLÜ
Koray KÖYLÜ

Posted on

IP geolocation with zero external APIs, the Cloudflare Workers cf object

When I built whatsmy.fyi, I assumed I'd need a geolocation provider: MaxMind, ipinfo, ip-api, pick your poison. They all mean the same thing: an external dependency, an API key, a quota, added latency, and someone else's server seeing your users' IPs.

Then I found out Cloudflare Workers makes the whole category unnecessary.

The cf object

Every request that hits a Cloudflare Worker carries a request.cf object, populated at the edge before your code even runs. No lookup, no latency, no key. Here's what's inside:

{
  asn: 34984,                    // ISP's autonomous system number
  asOrganization: "Superonline", // ISP name
  city: "Istanbul",
  region: "Istanbul",
  country: "TR",
  continent: "AS",
  isEUCountry: undefined,        // "1" if EU, undefined otherwise
  latitude: "41.01380",          // string, not number!
  longitude: "28.94970",
  postalCode: "34000",
  timezone: "Europe/Istanbul",
  colo: "IST",                   // which CF datacenter handled this
  clientTcpRtt: 12,              // user's RTT to the edge, in ms
  httpProtocol: "HTTP/3",
  tlsVersion: "TLSv1.3",
  tlsCipher: "AEAD-AES128-GCM-SHA256"
}
Enter fullscreen mode Exit fullscreen mode

That last group surprised me most: you get the user's HTTP protocol, TLS version, and actual TCP round-trip time for free. Try getting that from a geo API.

A complete IP endpoint in ~30 lines

export default {
  async fetch(request) {
    const cf = request.cf ?? {};
    const ip = request.headers.get("CF-Connecting-IP");

    return Response.json({
      ip,
      city: cf.city ?? null,
      country: cf.country ?? null,
      isp: cf.asOrganization ?? null,
      asn: cf.asn ?? null,
      timezone: cf.timezone ?? null,
      lat: cf.latitude ? parseFloat(cf.latitude) : null,
      lng: cf.longitude ? parseFloat(cf.longitude) : null,
      protocol: cf.httpProtocol ?? null,
      tls: cf.tlsVersion ?? null,
      rttMs: cf.clientTcpRtt ?? null,
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

That's the entire backend. No database, no GeoIP file to update monthly, no vendor.

The gotchas (learned the hard way)

1. Coordinates are strings. latitude: "41.01380" — parse them or your JSON consumers will suffer.

2. Every field can be missing. Tor exits, some corporate proxies, and brand-new IP ranges come through with sparse data. Null-check everything:

const city = typeof cf.city === "string" ? cf.city : null;
Enter fullscreen mode Exit fullscreen mode

3. isEUCountry is the string "1" or undefined. Not a boolean. You've been warned.

4. Local dev gives you an empty object. wrangler dev (and Next.js dev servers behind adapters) won't populate most fields. Guard for it, and test geo logic in preview deployments.

5. City-level accuracy is "usually right, sometimes hilarious." Same as every IP geolocation source, this data comes from the same underlying registries. Country is reliable; city is best-effort. Don't build anything that requires city precision.

The privacy angle

This architecture has a property I didn't fully appreciate until I wrote the privacy policy: the user's IP never leaves the request path. There's no third-party geo provider receiving your traffic logs as a side effect. Combined with not writing logs yourself, you can honestly say: nothing is stored, nothing is shared. That's the whole privacy section.

For whatsmy.fyi this is the entire data pipeline: the site, the free API, all of it runs on request.cf and nothing else. Sub-50ms TTFB, zero external calls, zero logs.

If you're on Workers and calling a geo API today, check request.cf first. The data was already there before you asked.

Top comments (0)