Blog
How Near Here Runs on Cloudflare
The infrastructure story: Workers, OpenNext, D1, Postgres + PostGIS via Hyperdrive + Tunnel + Workers VPC, three cache layers, and email split between Cloudflare and SES.
Published 25 May 2026
The pub quiz three streets away, the craft fair this weekend, the free concert at the church hall - they're all listed somewhere. Just not anywhere you'd think to look. Near Here checks hundreds of local venue websites and puts it all in one place. I wrote about why separately. This is a technical blog post about the infrastructure: how the serving stack fits together on Cloudflare, why we moved canonical search off D1, and what was interesting to build.
The shape of it
Near Here works by crawling hundreds of venue websites, council listings, and event feeds to build its catalogue. Those crawlers run on external cloud compute - deliberately off-platform, because long-running crawl jobs don't fit the Workers execution model. They write structured events into Postgres. Everything else lives on Cloudflare: the frontend renders on Workers, event queries reach Postgres via Tunnel + Workers VPC + Hyperdrive, platform state sits in D1, assets live in R2, and Cloudflare's edge cache, bot protection, and rate limiting sit in front of the lot. The boundary is clean: crawling and canonical storage live where they need long-running processes and spatial indexes; serving, caching, and everything visitor-facing lives where Cloudflare is strongest. Bulk email (the weekly newsletter) goes through Amazon SES. D1 sits in between, holding the crawler's queue state and operational records close to Workers.
The rest of this post walks through each boundary and why it landed where it did.

Why Cloudflare
If you're deciding whether Cloudflare fits your project, here's what decided it for ours. The tradeoffs are real too, and I cover them below.
The constraints force good decisions. No long-running processes, no filesystem, hard limits on CPU per request, capped bundle sizes. On other clouds you can do anything, which sounds great until you've spent a fortnight plumbing IAM and VPCs before shipping a line of product code. Workers, Hyperdrive, D1, R2, KV, Tunnel: each one is scoped narrowly enough that you can hold the whole thing in your head.
You can go from nothing to a real product fast. I'd never used Cloudflare before Near Here. What saved me was that Cloudflare publishes their docs in a format built for LLMs, so you get accurate answers because the model has the real docs. Going from zero Cloudflare experience to a live product without properly getting stuck is something I couldn't say about any other cloud I've worked on. Their Discord helped too: product updates and beta announcements land there, which made it easy to pick up new features the day they shipped.
The Startup Program gave us room to build properly. Near Here is on Cloudflare's Enterprise tier via their Startup Program: enterprise-grade bot protection, generous headroom, advanced rate-limiting and managed rules. Credits don't last forever and the architecture is designed for the day they expire, but starting on Enterprise meant building the system properly first rather than contorting the product around every line item from day one.
We love D1. But the maths can hit hard.
Near Here started on D1, and for a while it was a perfect fit. SQLite at the edge, no connection pool to worry about, migrations handled by Wrangler. We loved it.
Then the catalogue grew, and one particular part of the maths stopped working.
The killer query is "events near you." A visitor lands on the homepage, we've got their lat/lng, and we want to return the events happening within a sensible radius, ranked by distance and date. At real-world catalogue sizes this is fundamentally a spatial problem - the kind of thing PostGIS was built for. On a real database you build a GiST index on a geography column and the planner happily picks the few hundred candidate rows that could possibly match.
On D1 you don't have that. You can fake it with a bounding-box query on lat/lng columns - and we did - but the planner still has to read every row inside that box, then re-rank them on the application side. The performance was still good. That wasn't the problem. The problem was that, even with the best banding and indexing we could put around it, a single nearby-events lookup could mean more than 14,000 D1 rows read before the app had the handful of events it actually needed.
The reason it's a real problem (and not just an aesthetic one) is D1 charges per row read. So a spatial lookup that reads 14,000 rows costs 14,000 reads even if you only show six events on the page. Multiply by every visitor on every page-view that hasn't cached, multiply again by every region of the country, and you end up with a bill that grows roughly with catalogue size x traffic rather than traffic alone. That's the wrong curve.
To be fair: D1's pricing is cheap when your reads are indexed point lookups. Scale-to-zero, no egress fees, genuinely good value for the right workload. Our problem was specifically that a spatial query can't be an indexed point lookup.
So canonical event search moved to Postgres, where we can put a proper spatial index on the venue geography column and the planner does the right thing for free. D1 didn't go away. It remains a great fit for the platform state around the crawler: queues, source status, leases, reason codes, newsletter state, boosts, and the operational records that need to be close to Workers but don't need PostGIS.
This wasn't a latency benchmark. D1 was already fast enough. The useful measurement was the read shape: how much work each backend had to do before returning one nearby-events page. All the dashboard numbers in this post are from late May 2026.
D1 — 5-mile nearby query around Redhill
Postgres + PostGIS — same query, spatial index
Same nearby-events lookup, same 5-mile radius. D1 scans the full bounding box; PostGIS uses a spatial index.
The bridge back to Postgres
Once you decide the database lives outside Cloudflare, you have a new problem: getting a Worker to talk to it. Workers are stateless, short-lived, geographically distributed, and run inside Cloudflare's network. Postgres expects long-lived TCP connections from a small number of pooled clients. They don't naturally agree.
Cloudflare have spent the last couple of years building the bridge, and we use the whole bridge:
- Cloudflare Tunnel - the Postgres instance lives on external compute inside a private network. Tunnel gives us a secure path from Cloudflare to that network without exposing the database to the open internet.
- Workers VPC integration - lets a Worker treat the tunnel endpoint as a routable destination, so our application code can open a database connection as if Postgres were sitting next door.
- Hyperdrive - pools and re-uses connections at the edge, so we don't pay the TCP-handshake-plus-TLS-plus-auth cost on every request. It also caches query results, which means a surprising fraction of our Postgres queries don't actually reach Postgres.
Tunnel, VPC, and Hyperdrive all did what the docs said they would, first time. If you've done much infrastructure plumbing, you'll know that's not the default emotional state of the genre.
The result is that Postgres remains the canonical spatial database, but repeated reads don't all become database work.
Hyperdrive query cache hit rate
Most repeated database reads never reach Postgres.
Three caches, then Postgres
Add it all up and a request for "events near Reigate" passes through three cache layers before anything reaches Postgres:
- Cloudflare's edge cache - the page itself, rendered HTML, cached at the nearest data centre to the visitor. Static-ish pages (town pages, venue pages) hit this on most requests.
- The Worker - OpenNext's ISR layer plus our own in-Worker memoisation hold rendered fragments and serialised query results, so dynamic pages don't re-do work between requests.
- Hyperdrive - caches the query results themselves, deduplicating identical SQL across concurrent Workers. When two people in neighbouring towns are both trying to find the same village hall quiz night, Hyperdrive answers both from one actual database query.
Only when all three miss does the query reach Postgres. The spatial index keeps it cheap when it does.
Keeping the caches in agreement is deliberately simple: TTL-based expiration everywhere, no explicit invalidation. Events rarely change once published, so a short staleness window is an easy trade for never having to think about cache-coherence bugs at three in the morning.
Maps are a smaller version of the same pattern. We self-host UK vector tiles as a PMTiles archive in R2. PMTiles is a single-file map tile archive format, which means we can store the whole UK extract as one object and fetch only the byte ranges needed for a requested tile. A tiny Worker translates tile requests into those byte-range reads, with Cloudflare cache in front.
1.41 GB
Entire UK tile archive
89k
Requests served
2.17 ms
Avg CPU per request
One R2 object, one small Worker, Cloudflare cache in front.
Email, in two halves
Email at Near Here splits cleanly down two paths, because the two jobs have nothing in common except the word "email".
Transactional email - signup confirmations, magic links, one-off notifications. These have to leave inside seconds of the user triggering them. We send them through Cloudflare's outbound email product, which we've been using since day one of the public beta. Sending an email from a Worker is just another API call.
Bulk email - the weekly newsletter. That's a different problem entirely: deliverability, warm-up, bounce handling, complaint loops, ten years of accumulated inbox-provider trivia. We use Amazon SES for that bit and let Amazon handle the long tail. Cloudflare's email product isn't aimed at bulk yet, and pretending otherwise to keep the stack pure would be a daft trade.
Stopping the obvious abuse. Every email form on the site - newsletter signup, contact, anywhere a visitor types an address - sits behind Cloudflare Turnstile. We didn't build a captcha; we delegated it to people who can fingerprint bots at scale. Enterprise bot protections, managed rules, bot signals, our own rate limits, and Turnstile all work together here. Turnstile is invisible to most real users and a wall to most automated ones.
Things that bit us
D1's row-read billing reaches further than you'd think. The spatial-query economics were the big one (above). But the same billing model surfaced in a less obvious place: offsite backups. D1 has Time Travel, which is excellent for operational recovery. But Time Travel lives inside Cloudflare. If you want a proper offsite backup, your data in a bucket you control, you need to export. A D1 export is a full scan of every table by definition, and D1 bills row reads on scan count, so the export bills every row it touches. Your backup cost scales linearly with your database size.
Location-based search is surprisingly hard to cache. Three cache layers, and for a while none of them were doing much for the query that matters most: "events near me." Every visitor has a slightly different lat/lng. The events don't change between requests, but the search origin does, and a cache keyed on a precise coordinate almost never gets a hit. Rounding to a small lat/lng grid only works in high-traffic areas; everywhere else, still no hits. The fix was to snap each visitor's location to the nearest town, village, or neighbourhood in our location graph and cache against that. The areas can be smaller than a square mile, so results still feel hyperlocal. But instead of millions of possible cache keys (one per coordinate), we've got a finite set of known locations and a cache that actually works.
OpenNext on Workers is a tradeoff, not a free lunch. The frontend runs Next.js via OpenNext on Cloudflare Workers. There are caching behaviours that don't translate cleanly to the Workers runtime, edge cases where the abstraction layer shows its seams, and moments where you're debugging the adapter rather than your own code.
The crawler is written in Python, and if I could have built the frontend in whatever I wanted, I probably wouldn't have picked Next.js on Workers. But OpenNext gives you a low-cost, globally distributed frontend with minimal operational overhead. The price is that you're working within two sets of constraints, Next.js's and Workers', and occasionally they disagree. For what I needed, a fast, cheap, server-rendered frontend that deploys in seconds, it was the right call. But go in with your eyes open, especially around caching behaviours you'd expect to reduce Worker CPU time but can't.
What's next
The weekly newsletter is live - personalised local picks delivered to subscribers' inboxes every week. On the immediate roadmap: Boosts. The idea is that anyone in the community can boost the visibility of an event they care about. Say I don't have time to help out at the school fête - I can still drop the price of a pint to make sure more people find it. Community-powered promotion. I wanted community to be at the heart of everything Near Here does.
If you're building something similar
I worked at AWS for over five years, building customer-facing AI prototypes. But I've been enthusiastically recommending Cloudflare to everyone who asks since I started Near Here. The gap between "I have an idea" and "it's live" is incredibly short. Five dollars a month gets you Workers, KV, R2, D1, Durable Objects, and Queues. You literally only spend time building your actual product.
Start on Cloudflare, lean into the constraints, and reach outside the platform only when you hit a real wall. We reached outside three times: external compute for crawling, Postgres for spatial queries, and Amazon SES for bulk email. For Postgres, Cloudflare built the bridge back in with Tunnel, VPC, and Hyperdrive. Everything else lives on Cloudflare because it was simpler to keep it there than to move it somewhere else. That's the best compliment I can pay any infrastructure: it stayed out of the way.
Near Here is at nearhere.events if you fancy a poke around. Somewhere near you, right now, there's a quiz night warming up that you don't know about yet.
- Jon, founder of Near Here
hello@nearhere.events