Count the sequential round trips. The count picks the side.

One countable property of a request settles it. Not the category the workload belongs to, and not a bucket list. Only the number of sequential trips the request makes to the data it needs.

  1. Sequential trips 0

    0 ms of data wait

    Belongs at The user's edge Nothing to wait for, so distance to the data costs nothing.

  2. Sequential trips 1

    about 25 ms either way

    Belongs at Close to a wash One crossing costs the same near the user or near the data.

  3. Sequential trips 2 or more

    about 25 ms per trip, unplaced

    Belongs at Beside the data Every extra trip multiplies the distance, so the chain pays for it.

The 20 to 30 ms band is Cloudflare's published figure for an unplaced Worker reaching a database, and 1 to 3 ms is the placed figure. Every crossing drawn above is one of those round trips.

Edge vs origin: which parts of an application belong at the edge

One countable property of a request settles the placement. Count the sequential round trips it makes to its data, then apply the rule per route.

Why the category lists never settled edge vs origin#

Sorting workloads by category cannot answer the edge vs origin question. In practice, two implementations of the same category land on opposite sides. The deciding difference is never the category name, but rather how that implementation talks to its data.

Two categories, four implementations, opposite sides of the same line
ImplementationCategory the guides file it underSequential trips to dataWhere it belongs
Signed token verified locallyAuthentication, "safe at the edge"0The user's edge
Session row read from the primaryAuthentication, "safe at the edge"1A wash, nothing moves
Marketing page from bundled contentRendering, "it depends"0The user's edge
Dashboard render chaining five lookupsRendering, "it depends"5Beside the data

Authentication, filed under safe at the edge#

Take authentication, which every guide files under "safe at the edge". A check that verifies a signed token locally makes zero data trips, so the edge wins outright. In contrast, a check that reads a session row from the primary makes one trip, so the edge buys nothing at all. Same category, opposite answers, and the deciding difference was the data path.

Rendering, filed under it depends#

Now take rendering, which those same guides file under "it depends". A marketing page rendered from bundled content makes zero trips. A dashboard render chaining five lookups makes five, so the bucket list was never the tool.

The one property that decides edge vs origin#

Placement is decided by the number of sequential round trips a request makes to the data it needs. Only sequential trips multiply the distance between compute and data. A single trip does not multiply anything, so it moves nothing.

data_waitsequential_tripsper_trip_cost

Cloudflare's Smart Placement documentation (opens in new tab) makes the single-query case explicit. A Worker making only one database query per request gains nothing from placement near the database. Because that 20 to 30 ms round trip costs the same near the user or near the data, nothing moves. The boundary moves only when sequential queries demand colocation.

Because the property is a count, the rule is falsifiable. For example, you can hold it against one endpoint and watch it fail.

The arithmetic behind edge vs origin, run on one endpoint#

Run the numbers and the break-even shows up in under a minute. Take an illustrative /api/dashboard endpoint on a global product. It issues five sequential queries for user, org, entitlements, recent activity, and unread count. The database is a single-homed Postgres in aws:us-east-1. Also, the user sits in Sydney, which is the worst case for this endpoint.

Unplaced: five crossings at the full price#

Unplaced, every query crosses the Pacific and returns.

125 ms5 sequential queries25 ms per trip

The 25 ms is the midpoint of Cloudflare's published 20 to 30 ms band for an unplaced Worker.

Placed: the same chain at local prices#

Placed in the database region, the same chain pays the published 1 to 3 ms.

10 ms5 sequential queries2 ms per trip

So the swing is about 115 ms, and it scales linearly with the chain. At ten queries the same one-line change moves roughly 230 ms. Substitute your own measured segment latencies and the working still holds.

Run the same switch against your own numbers#

A worked example is one endpoint. So set the depth and the two per-trip costs, then flip placement on and watch what it does to two routes at once.

Placement break-even lab: one switch, two routes, opposite outcomes

Set one endpoint, then flip the placement switch

Smart Placement

One configuration value, applied to the whole Worker.

/api/session

0 sequential trips

Signed token verified locally

Prefers this placement

0 mson this placement

Data wait0 ms
Forward hop0 ms
On the other placement25 ms

Better here by 25 ms

/api/dashboard

5 sequential trips

5 sequential queries

Prefers the other placement

125 mson this placement

Data wait125 ms
Forward hop0 ms
On the other placement35 ms

Worse here by 90 ms

Placement off: every crossing is a full round trip#

The chain is paying

The zero-trip route resolves at the user's edge for 0 ms. The 5-trip route pays 125 ms, because every one of its 5 crossings costs the full 25 ms. Placing this Worker would move about 115 ms of data wait off the chained route, and it would cost the zero-trip route a forward hop it currently avoids.

Swing on the chained route (depth x per-trip gap)115 ms
Break-even depth at these costs2 sequential trips
Both routes on both placements, using Cloudflare's published per-trip bands
RouteSequential tripsUnplacedPlaced
/api/session00 ms25 ms
/api/dashboard5125 ms35 ms
Set the sequential depth and the two published per-trip bands, then flip Smart Placement. The zero-trip route and the chained route recompute from that one switch, so you can watch it help one and hurt the other. The verdict, the readouts, and the data table are the accessible source of truth. Every figure applies Cloudflare's published bands to an illustrative endpoint, never a benchmark.

Notice which way each route moves. Because the zero-trip route waits on nothing, placement can only add a forward hop to it. Meanwhile the chained route clears its break-even at two trips and keeps improving from there.

How to count sequential depth in your own code#

Sequential depth is countable from the source, so for every await in the handler, ask one question. Is its input the output of the await before it? If yes, it is a new sequential trip. If no, it belongs to the same trip.

dashboard.batched.ts · ts
// ONE sequential trip. Nothing here reads its input from anything
// else here, so all four lookups leave together and the request
// pays a single round trip to the data.
const [user, org, entitlements, unread] = await Promise.all([
  db.user.findById(userId),
  db.org.findById(orgId),
  db.entitlements.findByOrg(orgId),
  db.notifications.countUnread(userId),
]);

return renderDashboard({ user, org, entitlements, unread });

The two panels move the same four rows and land on opposite counts. The wall clock difference between those shapes is roughly 75 ms in the example above, and no data changed hands differently.

In practice most dashboard code chains by habit rather than by need. The count is a property you control, not one you inherit. That matters more than the placement decision it feeds.

How to confirm the count before you move anything#

A count is a prediction, so confirm it against the deployed request first. The confirmation costs less than any placement change it might justify.

Instrument every awaited data call#

Instrument each awaited data call from inside the deployed Worker, recording the start and end of every call. Then sum the per-trip costs and compare the observed total against your predicted count multiplied by the per-trip cost. When the two disagree, the code has a trip you did not read.

instrument-trips.ts · ts
// Wrap every awaited data call, then compare the observed total
// against your predicted count times the per-trip cost.
const PER_TRIP_MS = 25; // your own measured segment latency
const trips: { name: string; ms: number }[] = [];

async function trip<T>(name: string, run: () => Promise<T>): Promise<T> {
  const start = Date.now();
  try {
    return await run();
  } finally {
    trips.push({ name, ms: Date.now() - start });
  }
}

const user = await trip('user', () => db.user.findById(userId));
const org = await trip('org', () => db.org.findById(user.orgId));

const observed = trips.reduce((total, t) => total + t.ms, 0);
const predicted = trips.length * PER_TRIP_MS;

// When observed and predicted disagree, the handler has a trip you did not read.
console.log({ trips, observed, predicted });

Where the CPU number will mislead you#

Also check where you are reading the number.

Why lowering the count beats moving the compute#

Removing a sequential trip improves every placement option at once. Moving the compute improves exactly one option, which is why the count comes first.

Two platform mechanisms remove specific trips without touching your topology. Hyperdrive pools connections and caches queries, which removes the connection-setup round trips a cold database connection pays on each request. Its behaviour is documented in how Hyperdrive works (opens in new tab). D1 read replication (opens in new tab) puts read-only copies near users, which removes the cross-ocean trip from read paths entirely.

The same five-query request with its trip count lowered three waysOne request, three ways to lower the count without moving the compute: batch the independent lookups so the chain collapses, pool the connection so the setup trips disappear, or put a read replica near the user so the read path stops crossing the ocean. All three improve every placement option at once.

In addition, all three changes leave your topology alone, so try them before you argue about edge vs origin at all.

The work that cannot move to the edge#

Some work does not move, whatever the count says. Its budget is CPU or process lifetime rather than waiting, so relocation changes nothing.

For example, video transcoding, large image pipelines, heavy cryptography, and long-running batch jobs all sit here. The Workers runtime bounds request duration and memory per isolate, which is what the platform limits (opens in new tab) page enumerates. When a workload exceeds that shape, the honest answer is a different primitive, not a different region.

For that case Cloudflare now ships Containers (opens in new tab), with published limits (opens in new tab) of its own, so read those numbers first.

When the state, not the code, decides the placement#

Sometimes the state pins the answer and the code follows. For instance, writes are the clearest case of the state deciding the placement. A write goes to the primary regardless of where the handler runs, so no placement makes a write path local.

Per-entity state behaves differently again, and it behaves better. A Durable Object holds its storage beside its own compute, so state and code share a location by construction. Its trip count is one by design, and it stays one.

Although that sounds like a free win, it comes with a throughput ceiling per object. Read the object model before you commit a hot path to it.

When NOT to use the edge vs origin rule#

Several cases disqualify this analysis before the counting starts, so say so out loud rather than quietly. The table gathers them, including the two that the sections either side of it cover.

The disqualifiers: cases that settle before the counting starts
DisqualifierWhat the budget really isWhat the count saysWhat to do instead
CPU or lifetime bound workCPU time and process lifetimeNothing, because the count measures waitingA different primitive, not a different region
Traffic already regional and near the databaseA distance you do not payThere is nothing to moveLeave the placement alone
Write-dominated routesThe trip to the primaryWrites reach the primary either wayReplication does not help, so read the write path
High tolerated stalenessA cache miss you rarely takeZero trips, by definitionCache the response and the question disappears
Residency and contractual commitmentsThe agreement, not the latencyOverruled before it is computedHonour the region the agreement names

First, if traffic is regional and already near the database, placement solves a problem you do not have. Second, if writes dominate, replication does not help, because writes still reach the primary. Third, if a route's budget is CPU rather than waiting, no relocation changes the number.

Finally, if tolerated staleness is high, cache the response and the trip count falls to zero by definition. In that case caching is the whole answer, and edge vs origin never comes up.

What the count cannot see: residency and contracts#

The arithmetic is blind to contracts, and contracts usually win. For instance, data residency commitments can pin a region whatever the latency arithmetic prefers. GDPR, HIPAA, and SOC 2 obligations all show up this way.

Consider an illustrative buyer, a European health platform whose processing agreement names one region. In practice its dashboard would benefit from a replica in Sydney. Yet the agreement forbids it, so the count is overruled before it is computed.

This is not legal advice. Consult a licensed attorney. The buyer above is illustrative and not a client.

The failure mode: moving compute on a model alone#

The common failure is acting on the count before measuring it. Because placement is only a configuration value, it feels reversible. Its costs, though, are not distributed evenly across your users.

Two regressions land on the same distant user#

First, that user now pays a forward hop on every request, including requests that never touch the database. Second, any static asset the Worker serves is now served from the placed region rather than from the user's own edge.

Two request traces side by side, one resolved at the user's edge and one crossing once to the placed regionThe same application, two routes, two placements. Trace A verifies a signature locally and returns 401 from the user's edge with no data trip at all. Trace B crosses the Pacific once, then runs its five queries at about 2 ms each beside the database. Placing the whole Worker would have moved Trace A onto that crossing for nothing.

The safe shape is to split the routes#

The safe shape is to split the routes, not the application. Keep the zero-trip routes unplaced, and pin only the chains. Apply the rule twice.

Where this edge vs origin rule goes next#

The next depth is the same decision applied to a concrete workload. For rendering, the walkthrough of React rendering on Cloudflare sorts SSR, SSG, and edge rendering by this same per-route logic on the same runtime.

The 2026 site optimization guide frames the wider latency budget a page lives inside. For measuring the payoff afterwards, the field guide to Lighthouse performance audits for mobile-first work covers the instruments. The other half of the argument, the work that belongs beside the database, is easier to see through common MySQL performance bottlenecks.

The one edge vs origin call worth a second opinion#

Most of this is safely reversible within a sprint, though one part is not. A residency decision, or a state model pinned to a region, is expensive to unwind after the data lands there.

If that is the call in front of you, our DevOps and infrastructure team will walk the route table with you before anything moves. Atyantik has run 50+ enterprise engagements across seven countries since 2015. This counting exercise is usually where those engagements start. No pitch deck, just the count applied to your own traffic.

Walk the route table with our DevOps team
Portrait of Tirth Bodawala

Tirth Bodawala

Chief Technology Officer, Atyantik Technologies

Tirth leads software engineering at Atyantik Technologies, a software product studio building web platforms, mobile apps, and AI-integrated systems since 2015. He writes about shipping software that holds up in production, from rendering performance to the platform decisions behind a launch.

More from Tirth BodawalaRendering React on CloudflareHire DevOps engineers

Keep reading