Development

How to Move a Database Query to the Edge Safely

Antonio Evans10 min read

A split diagram: on the left, a primary database with a write path; on the right, an edge runtime node handling read queries.
Key points

Key points

  1. 1Move only read-only queries whose consumers can tolerate a stated staleness window; everything else stays on the primary.
  2. 2The edge runtime cannot hold a database connection open, so every query path needs a pooler or an HTTP data API in front of it.
  3. 3Replication lag is a number you must measure, alarm on and act on, with a hard threshold that forces fallback to origin.
  4. 4Roll out behind a flag per query, with a tested one-command rollback that does not depend on a redeploy.

Step 1: Audit which reads actually qualify

Start with an inventory, not with architecture. Pull the query log for a representative week and group statements by normalized fingerprint, then attach four facts to each: call volume, p50 and p99 latency, which tables it touches, and who consumes the result. Only the combination of "read-only, high volume, latency-sensitive, staleness-tolerant" earns a place in the first wave.

Two filters do most of the work. First, reject anything that runs inside a transaction that also writes, because moving half a transaction to another host breaks atomicity. Second, reject anything whose result feeds directly into a write decision (balance checks, inventory reservations, uniqueness validation). Shopify's engineering team describes the core hazard precisely: an individual stale read is usually harmless, but a set of related reads split across replicas with different lag can assemble an incoherent picture, and if that picture informs later writes, the damage compounds read consistency with replicas.

For the survivors, write down the staleness budget in seconds. This is a product decision, not an engineering one, and it should live in the same repository as the query:

Query classExampleStaleness budgetEligible for edge?
Catalog browseProduct listing, category pages60sYes, cache first
Profile displayPublic profile, avatar, bio30sYes
Feed / timelineSocial or activity feed5sYes, with lag guard
Search suggestionsAutocomplete2sOnly with local index
Cart contentsLine items before checkout0 (read your writes)No
Inventory countStock before add-to-cartNear 0No
Balance / entitlementWallet, plan limits0No
Auth session lookupToken to user resolution0Session store only

The last row deserves emphasis because it is where teams get burned. Authentication looks like a read, but a stale result means a revoked token still works or a valid one is rejected. Keep that lookup in a store designed for it rather than in a replicated relational table.

Output of step 1: a short ranked list, ideally five to fifteen queries, each with an owner, a staleness budget and a documented consumer contract.

Step 2: Understand what you are moving onto

The execution environment dictates more of the design than the database does. Edge runtimes are built on V8 isolates rather than containers or full Node.js processes: a single runtime instance hosts hundreds or thousands of isolates, each with isolated memory, and an isolate can start roughly a hundred times faster than a Node process on a container how Workers works. That startup speed is what makes per-request cold starts tolerable, and it is also why nothing survives between requests reliably. Isolates are evicted for resource limits or host pressure, so the runtime documentation explicitly advises against storing mutable state in global scope how Workers works.

The practical consequence: no persistent TCP connection, no connection pool living inside your function, no local in-memory cache you can trust. The API surface is web-platform oriented, with fetch, Web Crypto, streams and URL primitives, and Node core modules such as fs, net and child_process are absent or polyfilled edge runtime vs Node.js runtime. Most relational drivers assume a socket, so most relational drivers do not work unmodified. This is the single most common cause of "it worked locally" failures when a query is moved to the edge Next.js edge runtime limitations.

Also budget for the round trip you are trying to remove. A user in Singapore hitting an edge node next door still pays the full network cost to reach a database in Virginia. A Supabase user measured a trivial "make a few DB calls" function going from about 250ms in us-west to 1.1s in us-east and 2.3s in Southeast Asia, because the function ran near the user while the database did not Supabase region discussion. The lesson generalizes: an edge function in front of a distant primary is often slower than a regional function next to the primary. You need the data near the compute, not just the compute near the user.

Step 3: Put a replica and a cache in the read path

There are two ways to get data physically close to edge nodes, and serious deployments use both.

The first is a read replica in each region you serve. Replication copies data across machines, and the trade-off is explicit: asynchronous replication improves write performance but creates the risk that replicas lag behind the primary data replication strategies. Synchronous replication guarantees no stale reads at the cost of write latency proportional to the distance to replicas, which is usually unacceptable for cross-region setups a guide to database replication. For edge reads, assume asynchronous and design for lag.

The second is a cache at the edge, which is really an origin server model. A CDN edge serves cached copies and pulls fresh content from your origin, and the pull approach is generally the more effective way to keep edge copies current origin server vs edge server. Map that onto your query: the "origin" is your regional read replica or data API, and the edge cache holds serialized query results.

The rule most teams get wrong is cache key construction. A cache key must include every input that changes the answer: query fingerprint, all bind parameters, tenant or user scope where results are user-specific, and the schema or data version if you deploy migrations. A key that omits tenant scope is a cross-tenant data leak, not a performance bug.

function cacheKey(fingerprint: string, params: Record<string, unknown>, scope: string) {
  const normalized = Object.keys(params)
    .sort()
    .map((k) => `${k}=${String(params[k])}`)
    .join("&");
  return `q:${fingerprint}:${scope}:${normalized}`;
}

async function readThrough(key: string, ttlSeconds: number, load: () => Promise<unknown>) {
  const hit = await edgeCache.get(key);
  if (hit) return hit;
  const value = await load();
  await edgeCache.put(key, value, { ttl: ttlSeconds });
  return value;
}

Set the TTL from the staleness budget in step 1, not from habit. A 60s budget means a 60s TTL is the ceiling and anything shorter is finer-grained freshness you may not need. Add a short jitter (plus or minus 10 percent) to TTLs so keys do not all expire on the same second and stampede the replica. And decide up front what happens when both cache and replica fail: for a catalog listing, serve a stale copy with an explicit staleness header; for anything that gates a transaction, fail closed and route to origin.

Step 4: Solve connection pooling for a stateless runtime

A relational database has a hard cap on simultaneous connections, and each open connection consumes server memory. If every edge invocation opens its own connection, a modest traffic spike exhausts the cap and the database starts refusing connections, which is exactly the failure mode demonstrated when a serverless function instantiated a fresh client per request and drove connection counts up with concurrent load databases on the edge and serverless.

Three patterns work:

  1. External pooler. A proxy such as PgBouncer or a managed pooler multiplexes many client connections onto few server connections. Your edge function still needs a way to reach it, so locate it in the same region as the replica and expose it over a protocol the runtime supports.
  2. HTTP data API. Put a small regional service in front of the database that accepts a parameterized query over HTTP and returns JSON. Edge functions speak fetch natively, so this sidesteps driver incompatibility and connection lifetime in one move. It also gives you a single place to enforce read-only credentials, row limits and timeouts.
  3. Serverless-friendly drivers or HTTP drivers. Some providers expose Postgres or MySQL over HTTP. These work at the edge, but each request still creates a logical connection, so the pooler question moves to the provider's side and you should confirm its limits before assuming it scales.

Whichever you choose, enforce four things at the boundary: a read-only role for the edge path, a hard statement timeout (three seconds is a reasonable starting point for interactive reads), a maximum row count, and query allowlisting so an attacker cannot shape arbitrary SQL through your endpoint. Check the pooler's own numbers before launch: active versus idle connections, wait time for a connection, and the ratio of server connections to pool size. If average wait time climbs while query time is flat, you are sized wrong on connections, not on compute.

Step 5: Measure and tolerate replication lag

Lag is the variable that decides whether edge reads are acceptable. You cannot manage it without a number, so export two metrics from every replica: the replication delay in seconds, and the timestamp of the newest transaction visible on that replica. The second is what lets you make a per-request decision.

Two consistency tools are worth knowing. Causal consistency, implemented with a global transaction identifier, lets a read be made conditional on the replica having caught up to a specific known state, which is nearly as strong as a fully consistent read read consistency with replicas. Monotonic read consistency, the simpler approach Shopify settled on, routes a series of related reads to the same server so the client never sees time run backwards, even if it sees slightly old data read consistency with replicas.

For edge work, monotonic reads plus a lag threshold covers most cases. Concretely:

  • Version every mutation response with a replication watermark (a GTID, an LSN, or a row in a data_version table).
  • On a read that must reflect the user's own last write, send the watermark and require the replica to be at or past it. If it is not, wait briefly once (for example 100ms, retried twice) and then fall back to origin.
  • On ordinary reads, compare the replica's lag metric with the query's staleness budget from step 1. If lag exceeds the budget, skip the replica and serve cache; if the cache also misses, go to origin.
SignalWatch thresholdAction
Replica lag, p50Above 0.5sLog, no action
Replica lag, p95Above the tightest active budgetServe cache with stale header
Replica lag, sustainedAbove 5s for 30sRoute the affected query class to origin
Replica lag, criticalAbove 60s or replica stoppedDisable edge path for that query, page on-call
Read-your-write miss rateAbove 1 percentRaise watermark wait budget or move query to origin
Pooler connection wait p95Above 20msAdd pool capacity or reduce concurrency
Edge error rate on queryAbove 0.5 percent for 5 minutesFlip the query flag off

These thresholds are starting values, not industry constants. Calibrate by replaying production traffic and watching which queries actually return stale data at each lag level.

Step 6: Roll out behind flags with a tested rollback

Treat the migration as a series of reversible states, not as a launch. The unit of control is a single query, not the whole application, and the switch belongs in configuration the edge runtime can read at request time.

A workable sequence:

  1. Ship the new code path disabled. It exists but every request takes the old route.
  2. Enable for internal users only, so you compare old and new results on real data. Sample responses and diff them; a mismatch rate above zero on fields that matter is a stop signal.
  3. Enable for 1 percent of external traffic for at least a full traffic cycle, including your busiest hour.
  4. Move to 10 percent, then 50, then 100, holding each step until error rate, p99 latency and stale-read metrics are flat or better than the control.
  5. Keep the old path compiled in for a defined period after 100 percent, and keep the flag.

Rollback triggers should be written in advance and owned by whoever is on call: error rate, p99 latency regression beyond an agreed margin, any stale-read incident affecting a write decision, or lag sustained over the critical threshold. Rolling back should mean flipping one flag, not reverting a deploy, because a deploy in a global network is slower and riskier than a config change. Verify by actually executing the rollback in staging before you need it.

One more check that frontend-first guides skip: verify that what the edge returns is what the primary would return, over time and not just in a sample. Run a shadow read against origin for a small percentage of requests, compare normalized payloads, and alert on divergence. Divergence that is not explained by lag means a caching bug, a missing key parameter or a schema mismatch, and those do not self-heal.

Step 7: Decide where the compute should actually live

Not every query ends up at the edge, and that is a fine outcome. The distinction between serverless functions in broad regions and edge functions in many locations is a latency decision, and the same workload can be worse at the edge if its data is regional understanding serverless and edge functions. Edge platforms reduce the distance between user and code, and early implementations of edge runtimes were built on precisely that premise edge functions explained. They do not reduce the distance between code and data unless you replicate the data.

A decision rule that has held up: if the query's round trip to data dominates its total latency and the data cannot be replicated to the region in question, keep the query regional and optimize the data path instead. If the data can be replicated and the staleness budget is nonzero, move it. If the result is identical for every user, cache it at the edge and skip the replica entirely.

For teams that want the query path, cache layer and supporting services assembled rather than hand-rolled, Quanta can build the front end and the regional service that fronts the replica, which is often the fastest way past the driver and pooler work in step 4 (https://quantaintelligence.ai/). Where the query results then feed a model call in the same request, for example a personalized recommendation or a generated summary, routing that call alongside the data path is worth designing at the same time.

FAQ

FAQ

Can any read query be moved to the edge?

No. It must be read-only, tolerate a stated staleness window, and not feed directly into a write decision. Reads inside write transactions, uniqueness checks, balance and inventory lookups, and authentication lookups should stay close to the primary or in a purpose-built store.

Why can't I just open a database connection from an edge function?

Edge runtimes are based on short-lived V8 isolates that are evicted without warning, and they lack the Node core modules most database drivers depend on. With no reliable connection lifetime and no local pool, per-request connections exhaust the database's connection cap under load. Use an external pooler, an HTTP data API, or a serverless-friendly driver.

How much replication lag is acceptable?

Exactly as much as the query's product-defined staleness budget. Useful starting thresholds: serve cache instead of the replica once latency exceeds the tightest budget, route to origin when lag is sustained above 5 seconds, and disable the edge path entirely above 60 seconds or when the replica stops reporting.

What is the difference between causal consistency and monotonic reads here?

Causal consistency uses a transaction identifier so a read can require a replica to have caught up to a known write, giving near-strong guarantees at the cost of extra coordination. Monotonic reads simply route related reads to the same replica so the client never sees older data after newer data, which is cheaper and sufficient for most display queries.

How do I prove the migration did not break consistency?

Run shadow reads against origin for a percentage of traffic, diff normalized payloads, and alert on divergence that replica lag does not explain. Pair that with a per-query feature flag, a written list of rollback triggers, and a rollback you have executed in staging rather than only designed.

Sources

Sources

  1. Read Consistency with Database Replicas
  2. How Workers works - Cloudflare Docs
  3. Edge Runtime vs Node.js Runtime: When Your Serverless Functions ...
  4. How to Fix 'Edge Runtime' Limitations in Next.js - OneUptime
  5. Is it possible to choose which region(s) the Edge functions run in?
  6. Data Replication Strategies
  7. A Guide to Database Replication: Key Concepts and ...
  8. What is an Origin Server | Origin vs Edge Server | CDN Guide - Imperva
  9. The Problem with Using Databases on the Edge / Serverless.
  10. Understanding the Difference Between Serverless Functions and ...
  11. Edge Functions Explained | Netlify Blog
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles