FuelRoute: From Overpaying in Sussex to a CloudFlare Worker
I overpaid for petrol while driving through Sussex to look at a horse I might buy. That’s not the most embarrassing way I’ve wasted money, but it’s unusually specific.
I didn’t know the area, had no signal for much of the drive, and filled up at the first station I saw. A few miles down the road, a friend who knew the region pointed out a station with prices 10p a litre cheaper. By then I’d already paid for a full tank. That difference seems minor until you realise it happens constantly to drivers everywhere — uncertainty about whether you’re getting ripped off because you can’t see what’s available nearby.
On the drive back, I heard it on the radio: the UK Government had just launched Fuel Finder. Under The Motor Fuel (Open Data) Regulations 2025, every petrol station in the country is now legally required to report its prices within 30 minutes of any change.
It’s run by the Department for Energy Security and Net Zero, with the Competition and Markets Authority holding enforcement powers. The infrastructure had landed. The data existed.
And the first thing I thought was: Apple CarPlay. Not a mobile app you pull out while driving. Drivers need this information while they’re deciding where to fuel up, right there on their dashboard.
Building FuelRoute
I started on 13 February 2026 — eleven days after the scheme launched. The app pulls live fuel price data and displays it the way drivers actually need it: nearby stations sorted by price, distance, current cost. A map view so drivers can see where stations are geographically. A price picker with a liquid glass design where the differences pop visually. Route planning so you can decide whether it’s worth detouring to the cheaper station.
The initial version launched quickly. CarPlay entitlements working, location permissions handling reliably, a screenshot mode for App Store screenshots that skips live API calls. Privacy manifest. The plumbing that makes an app feel solid rather than hacky.
But the Fuel Finder API had constraints I couldn’t ignore.
The API Problem
The Fuel Finder API covers all 7,900+ UK stations in one place — comprehensive and authoritative. But it’s rate-limited to 30 requests per minute and requires OAuth 2.0 credentials. The developer guidelines are explicit: client secrets must not be embedded in mobile apps. Even if I ignored that rule, the maths doesn’t work. Each full data pull takes about 36 paginated API calls. If a few hundred users refresh simultaneously, the rate limit is gone in seconds.
I needed a proxy cache. Something that fetches the data once, caches it, and serves every user from cache. But I didn’t want to add another Docker container to my DigitalOcean droplet for what’s essentially “fetch JSON every 5 minutes, cache it, serve it.” That felt like using a sledgehammer to crack a nut.
Then I remembered I already use CloudFlare for DNS. And CloudFlare has Workers.
What Actually Is a CloudFlare Worker?
If you haven’t come across Workers before, the mental model is surprisingly simple. A CloudFlare Worker is a small piece of JavaScript or TypeScript that runs on CloudFlare’s edge network — the same infrastructure that handles DNS and CDN for millions of websites.
When a request hits a URL you define, CloudFlare intercepts it and runs your function on the nearest edge server instead of routing it to a traditional backend. There are over 300 of these edge locations worldwide.
The interesting technical detail: Workers aren’t containers. They’re not even VMs. They’re V8 isolates — the same JavaScript engine that powers Chrome. CloudFlare runs thousands of V8 instances on each edge server. When your Worker starts, it gets its own lightweight sandboxed execution context that spins up in under 5 milliseconds. No cold boot of an OS, no container image pull, no kernel. Just a JS runtime sandbox.
The trade-off is you’re limited to JavaScript, TypeScript, or WebAssembly. You can’t run arbitrary binaries. But for “fetch, cache, serve” that’s more than enough. CloudFlare’s CLI tool wrangler handles the rest — npx wrangler dev for local development, npx wrangler deploy to push to production.
The Architecture
The whole thing is straightforward:
UK Gov Fuel Finder API
→ CloudFlare Worker (OAuth + cache in KV)
→ CloudFlare Edge CDN (300+ locations)
→ FuelRoute iOS app / CarPlay
The Worker handles OAuth authentication, fetches station metadata and fuel prices from the government API, caches everything in CloudFlare KV, and serves four JSON endpoints: /stations, /motorway-stations, /health, and a manual /sync. How that cache actually gets refreshed turned out to be the most interesting problem of the whole build — more on that in the gotchas.
Total code: around 575 lines of TypeScript.
Getting It Into Production
The developer portal registration was straightforward — GOV.UK One Login, register as an “Information Recipient”, create an application, get a client_id and client_secret. The fair use policy shapes the design though: you must call the API at least every 5 minutes on average, present prices unbiasedly, and include a way for users to report discrepancies. All reasonable stuff, but it directly influenced how the Worker behaves.
Credentials go into CloudFlare’s encrypted secrets store via npx wrangler secret put — never touching disk or version control. This is exactly the separation the government API terms require: credentials on the server side, never in the mobile app.
The Gotchas
A few things bit me that aren’t obvious from the documentation.
The government API sits behind AWS CloudFront. My Worker runs on CloudFlare’s edge. When CloudFront sees a request coming from a known CDN/proxy IP range with minimal headers, it returns a 403. Local development worked perfectly — wrangler dev runs on your machine with a normal IP. Production failed silently. The fix was adding browser-like headers to all outbound requests:
const API_HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"Accept": "application/json, text/html, */*",
"Accept-Language": "en-GB,en;q=0.9",
"Referer": "https://www.fuel-finder.service.gov.uk/",
"Origin": "https://www.fuel-finder.service.gov.uk",
};
Two CDN providers, both doing their job, creating friction where there shouldn’t be any.
The developer documentation also referenced api.fuelfinder.service.gov.uk as the base URL. That domain doesn’t resolve. The actual base URL is www.fuel-finder.service.gov.uk/api/v1 — 20 minutes of confused curl commands before the DNS failure made it obvious. And the API returns raw JSON arrays at the top level, not wrapped in {"data": [...]} like most APIs, while the OAuth token response is wrapped. Consistency isn’t the API’s strongest feature, but the data is comprehensive and that matters more.
I also added a /sync endpoint that triggers the data fetch on demand. During development, waiting for the scheduled refresh to fire to see if a code change worked was painful. Being able to curl https://fuel.dreamfold.dev/api/v1/sync and immediately see either {"status": "synced"} or the full error message made debugging the CloudFront issue much faster.
And then there was the gotcha I’m fondest of — the one that quietly rewrote the architecture.
The obvious design was a cron trigger: every five minutes, CloudFlare fires a scheduled event, the Worker wakes up, syncs, fills the cache. Clean. It worked perfectly under wrangler dev. In production the cache simply stopped moving — the cron fired on schedule, and the government API never answered it.
It’s the CloudFront 403 again, one layer down, except this time headers can’t save you. A Worker’s scheduled invocation egresses from a different IP pool than its fetch invocation, and that pool is the one the government API’s WAF blocks. The request never gets far enough to be judged on its headers at all. The two obvious escape hatches both dead-end: route the call through a Service Binding, or have the cron self-fetch the Worker’s own URL, and you inherit the exact same blocked egress. Every way out of the scheduled context leaves through the wrong door.
So I deleted the cron. The refresh is now demand-driven — every /stations request checks how stale the cache is and, if it’s older than five minutes, kicks off the sync with ctx.waitUntil:
// Runs inside the fetch invocation — the egress the WAF actually accepts.
const ageMs = Date.now() - Date.parse(lastSync);
if (ageMs > FIVE_MINUTES) {
ctx.waitUntil(performSync(env, /* incremental */ true));
}
ctx.waitUntil lets the response go straight out on the cached data while the sync runs behind it, inside the fetch context the WAF is happy to serve. The iOS app polls every 30 minutes, and even a handful of users generate more than enough traffic to keep prices fresh. The scheduler I’d carefully designed turned out to be redundant — the users are the schedule.
7,987 Stations
Once the Worker was syncing, the numbers told the story. The government API returns 7,987 stations — every UK fuel retailer, mandated by law. Every independent, every supermarket forecourt, every motorway service station.
The API also includes fields like is_motorway_service_station, is_supermarket_service_station, amenities (24-hour fuel, car wash, toilets), and opening hours. The motorway flag meant I could finally add motorway filtering — a feature I’d been sketching out for weeks but couldn’t build without reliable data to back it.
Caching Strategy
This is where the engineering got satisfying. The Worker uses CloudFlare KV with different TTLs for different data:
- Station metadata — cached for 24 hours. Stations don’t move. New ones open rarely.
- Fuel prices — refreshed once the cache is more than 5 minutes old, but held in KV for 24 hours so stale prices survive an API outage.
- OAuth tokens — cached for just under an hour (they expire at 3,600 seconds).
Price syncs use incremental updates. The first fetch pulls everything; subsequent fetches pass an effective-start-timestamp parameter and only get prices that changed since the last sync. This keeps the request size small and stays well within the 30 requests-per-minute rate limit.
The iOS app refreshes from the Worker every 30 minutes. The Worker’s Cache-Control: public, max-age=300 header means CloudFlare’s edge CDN also caches the response — so even if thousands of users hit the endpoint simultaneously, most requests never reach the Worker at all.
Was a Worker the Right Choice?
I already run Docker containers on a DigitalOcean droplet for my other projects. A FastAPI container would have been the consistent choice — same stack, same deployment pipeline, same monitoring. It would have worked fine.
But the Worker is genuinely better for this specific job. It’s stateless, it’s edge-cached, it scales to zero when nobody’s using it, and I don’t think about it. The app fetches, the cache fills, the edge serves it. No container to monitor, no process to restart, no disk to fill up.
The cost model sealed it. The free tier gives you 100,000 requests per day — if you exceed it, the Worker stops responding rather than charging you. No surprise bills. The paid tier is $5/month for 10 million requests. To put that in perspective: 10,000 daily active users making 5 requests each is 50,000 requests, half the free tier. I’d need a genuinely successful app before this costs anything. And when it does scale, CloudFlare’s edge network handles it — 300+ locations, automatic load distribution, no capacity planning on my end.
Around 575 lines of TypeScript and a KV namespace. The government API sees one client making a handful of requests every few minutes, well within its rate limits. Every FuelRoute user gets fresh data from the nearest edge server. The rate limit problem that started this whole thing simply doesn’t exist anymore.
The whole journey — from overpaying in Sussex to a CarPlay app to a CloudFlare Worker proxying a government API — is the kind of chain reaction that only happens when boring infrastructure decisions create space for interesting products. The government opened the data. CloudFlare made the edge accessible. I just connected the dots.
FuelRoute is available on the App Store. The Worker source is on GitHub.