Ben Sutherland

Ben Sutherland

Head of Engineering & CTO

Read Resume
Articles
7 min read
ArchitectureServerlessCloudflareAzureIdentityPostgreSQL

Cross-Cloud Architecture: An Edge API on Cloudflare Workers with Azure

When running compute on Cloudflare while keeping identity and data in Azure is the right call — and the integration realities that decision brings.

Cross-Cloud Architecture: An Edge API on Cloudflare Workers with Azure

Mixing cloud providers is rarely the path of least resistance — each one quietly assumes you live entirely inside its ecosystem. But the constraints often make it the right architecture anyway: run compute where it's cheapest and closest to users, and keep identity and data where governance and existing investment already sit.

That was the reasoning behind an API I designed to run on Cloudflare Workers at the edge, authenticate through Azure AD, and store data in Azure PostgreSQL. The stack isn't the interesting part. What's interesting is the trade-offs that decision forces on you, and the point where both vendors' documentation politely stops helping.

Written in 2023. Cloudflare's database story and connection-pooling story have matured a lot since; the trade-offs below still hold, the rough edges are just smoother now.

Why mix providers at all?

Edge compute means your code runs in data centres around the world, close to wherever the request came from. For an API that means lower latency than a single-region deployment, plus no servers to manage, automatic scaling, and pay-per-request pricing. On its own, an easy sell.

But the organisation I was building for already lived on Azure. Their identity provider was Azure AD (now Entra ID), and compliance required data to stay in Azure's Australian regions. Those aren't preferences you engineer around — they're constraints you design to. So the shape became: edge compute from Cloudflare, identity from Azure AD, data from Azure PostgreSQL.

That combination wasn't documented anywhere, and it's worth being honest about why. Each vendor's examples assume you're using the rest of their stack. Nobody's incentivised to write the guide for stitching a competitor into the middle. Making it work meant understanding each service well enough to bridge the gaps myself — which is most of the job when you go cross-cloud.

The edge runtime is a different platform

The single most important thing to internalise early: an edge runtime is not a scaled-down server. It runs on lightweight isolates — the same class of engine a browser uses — without a full server-side standard library. No local filesystem, no raw network sockets, no spawning child processes. If a dependency reaches for one of those, it simply won't run, and it's better to know that before you discover it via a cryptic error.

The upside of that constraint is real: startup is measured in milliseconds, and isolates are cheap enough to spin up per request. You just have to bring runtime-compatible libraries and a router built for the environment rather than a traditional server framework. The local tooling faithfully simulates the runtime, which saved me from a lot of "works on my laptop, breaks at the edge" surprises.

The constraint worth designing around is time: edge platforms cap CPU and wall-clock time per request. For an API that validates input, hits a database, and returns a response, that's plenty. For anything doing heavy computation, it's a wall you need to know about before you hit it.

Identity: validating tokens at the edge

Users log in through the provider's OAuth flow, and the API receives a signed token (a JWT) carrying their identity and roles. Validating those tokens at the edge took some care, because the usual pattern assumes state you don't have.

Normally you fetch the provider's public keys from its JWKS endpoint, cache them in memory, and verify signatures against them. Edge isolates don't keep memory between requests, so "cache in memory" isn't available. The fix is to cache the keys in the platform's key-value store instead, so you're not hammering the provider's JWKS endpoint on every call. The flow:

  1. Extract the bearer token from the Authorization header
  2. Decode the token header to read the key ID (kid)
  3. Look that key up in the KV cache; on a miss, fetch from the JWKS endpoint and cache it
  4. Verify the signature with the public key
  5. Check the standard claims — issuer is the identity provider, audience is my app, token isn't expired
  6. Pull the custom claims for role-based access

The provider's app roles carry through into the token, so basic RBAC was straightforward: some endpoints required an admin role, others just an authenticated user, and middleware enforced it before the handler ran. For finer-grained access I layered my own logic on top — the token tells me who someone is and their broad role; my database decides what they're allowed to touch. Keeping that split clean is one of those decisions that saves you later.

The hard part: talking to the database

This was the genuinely hard part. A relational database speaks a TCP-based protocol. Traditional serverless can reach one because it has outbound TCP; edge isolates historically couldn't — they were HTTP-only.

That changed once the platform exposed a TCP sockets capability, which makes database connections possible. I used an edge-compatible database client that rides that capability and speaks the wire protocol, so from my code it looked like any ordinary query interface — parameterised SQL in, rows out. Clean. But the constraints underneath are where the architecture lives:

  • No connection pooling across requests. Each invocation is isolated, so there's no long-lived pool to lean on. Every request opens a new connection, and that costs you.
  • Connection setup isn't free. The TCP handshake plus TLS plus database authentication adds hundreds of milliseconds to the first query. If a request makes exactly one query, that setup is your latency.
  • Connection limits bite harder. A traffic burst means a burst of connections, and the database caps connections by tier. I had to size it around peak concurrency, not average load.

The platform's managed connection pooler is the intended answer here — it holds persistent connections on the provider's side so edge functions get reuse for free. I evaluated it and chose not to use it for this project: at the traffic levels involved, the added moving part wasn't worth it. That's a call I'd happily revisit at 10× the load — which is the point. "Right for now, with a known trigger to change" is a perfectly good architectural decision, as long as you write the trigger down.

Exposing the database (and the security trade-off)

A managed database blocks external connections by default, and private networking wasn't an option from the edge network. So it meant enabling public access, allowing the edge provider's published IP ranges through the firewall, requiring TLS on every connection, and verifying the certificate on the client side.

I want to be plain about this: it's less secure than a private-network deployment where the database has no public face at all. That's the honest cost of putting your compute on someone else's edge network instead of inside your own private network. You don't hand-wave it — you mitigate it deliberately: TLS everywhere, strong credentials, the narrowest firewall rules that work, and the platform's threat detection and auditing switched on to watch for connection patterns that shouldn't exist. Defence in depth isn't a slogan here; it's what makes the trade-off acceptable to sign off on.

Putting it together

A request end to end: it arrives at the edge and routes to a function; the function validates the identity token, checks roles for the endpoint, opens a connection to the database, runs the query, returns the response, and the connection closes as the function completes.

Latency was fine, not spectacular. Edge routing is fast, but the round-trip to the database's Australian region adds 50–100ms. For read-heavy paths, caching at the edge would take most of that back; for this workload it didn't need to.

What I'd carry forward

Cross-cloud is possible, but never free. Each vendor optimises for its own surface, so going between them means more docs, more uncovered edge cases, more careful testing. Budget for the integration tax up front rather than being surprised by it.

Edge compute is a different platform, not a faster server. Server-side assumptions break, per-request database connections are expensive, execution time is bounded. Wonderful for the right workloads; not a drop-in replacement for a long-running service.

Connection management is a design concern, not an afterthought. "New connection per request" is fine until it quietly becomes your bottleneck. Know your database's limits before load finds them for you.

The thread running through all of it is decision-making under constraint. Cross-cloud isn't sprawl when it's chosen deliberately — latency, compliance residency, and the organisation's existing investment are all legitimate forces. The architect's job isn't to pretend the seams away; it's to put them where you can see them, defend them, and change them on purpose later.

2026 Ben Sutherland