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"
}
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,
});
},
};
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;
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)