Inter-region physics, replication topologies, and a benchmark you can run yourself.
A distributed application can look healthy until its users spread out. You can add cores, tune queries, and buy bigger instances, and in-region traffic stays fast. But the moment application logic in Frankfurt reaches back to a primary database in Virginia on every request, you begin paying an ocean crossing on every dependent query, for every user, on every request that follows. No amount of vertical scaling removes that cost, because the cost is distance, and distance is bounded by physics.
Most experienced teams already know the standard responses, and for many workloads they are exactly the right ones. The narrower question is the one worth working through: when latency comes from distance rather than from slow queries, which of those responses actually help, and what does each one cost to run? Here is where the tax comes from, what your options are, and how colocating logic with data changes the arithmetic.
The cost is physical, and it compounds
Before choosing an architecture, it helps to separate the latency you can tune from the latency you cannot. Four costs come straight from distance, and none of them yield to a faster instance. A fifth cost, the business one, sits on top of the rest.
Latency has a floor. Light in fiber travels at roughly two-thirds of vacuum speed, about 5 microseconds per kilometer, so roughly 5 ms one way per 1,000 km (HPBN). New York to London has a fiber floor near 55 ms, and the purpose-built Hibernia Express cable was engineered down to about 58.95 ms RTT and gets no closer. AWS us-east-1 to eu-west-1 measures about 67.9 ms RTT (Economize). This is not a number a cloud provider can engineer away. It is the starting line, not a tuning target.
One round trip becomes ten. A TLS 1.2 handshake is two round trips before any query runs. Real endpoints are chatty: load the user, then permissions, then cart, then pricing, then inventory. If those five queries are dependent, that is five sequential round trips. At 90 ms Frankfurt to Virginia, that is over half a second of pure network wait before the database does a microsecond of work. Fan-out makes the tail worse, because a request inherits the slowest of its calls.
Availability multiplies, it does not average. Serial dependencies compound: ten components at 99.9% each yield roughly 99.0% combined. Make one of them a single-region database and its blast radius is your whole application, everywhere. On October 19 to 20, 2025, a race condition in DynamoDB's automated DNS management wiped the us-east-1 DynamoDB DNS records and cascaded for roughly 15 hours across more than 140 AWS services, taking down Snapchat, Fortnite, Ring, Venmo, and Amazon retail (ThousandEyes). The proximate cause was small. The blast radius was enormous, because so much depended on one regional data service. The lesson is not that DynamoDB is fragile. It is that centralizing a dependency centralizes its failures.
Egress is a recurring line item. Internet egress runs about $0.09/GB on AWS, $0.087/GB on Azure, and $0.085 to $0.12/GB on GCP. The quieter cost is internal: cross-AZ traffic is $0.01/GB each direction on AWS and GCP, so $0.02 per round trip on data that never leaves the cloud (EgressCost). Data transfer commonly runs 6 to 12 percent of a cloud bill. The EU Data Act is worth a caveat here: it bans switching charges from January 12, 2027, but Recital 99 explicitly preserves charges for ongoing parallel and multi-cloud use (Kemp IT Law). Free egress on exit changed the cost of leaving, not the cost of running a chatty path.
The business pays for the milliseconds. Treat the canonical numbers as directional, not gospel. Greg Linden's 2006 Amazon A/B result (100 ms of delay, about 1% of sales) is twenty years old and was never peer reviewed. Bing reported roughly 1.2% revenue loss per 500 ms at Velocity 2009. The defensible modern anchor is Core Web Vitals: Interaction to Next Paint at or under 200 ms at the 75th percentile. A cross-ocean round trip eats that entire budget in one hop.
Replication topologies, honestly
Two corrections first, because the usual shorthand hides the trade-off you actually face. CAP is not "pick two": it says that during a partition you choose consistency or availability. PACELC completes it: else, in normal operation, you choose between latency and consistency. That "else" clause is the one you meet on every request, and each topology below is a different answer to it.
Leader-based (PostgreSQL streaming replication, Aurora Global Database). One leader serializes writes and ships a log. Strong reads on the leader, stale reads on async replicas. Postgres streaming replication is async by default, so a committed transaction may not have reached a replica when the primary dies. Sync replication removes that loss and adds a full round trip to every commit. Aurora Global Database runs roughly 1 second RPO with unplanned failover RTO in minutes, and managed switchover dropped to typically under 30 seconds in May 2025. Reads scale, writes do not.
Multi-leader (DynamoDB Global Tables, Cosmos DB multi-region writes, MySQL Group Replication). Every region takes writes, then they reconcile. DynamoDB's default replicates asynchronously and resolves conflicts by last-writer-wins on item timestamps; the newer multi-Region strong consistency mode replicates synchronously and returns retryable conflict errors instead. Cosmos DB cannot offer strong consistency with multi-region writes enabled, and defaults to last-writer-wins on _ts. Last-writer-wins always converges, and it silently discards the losing write. Fine for a cache entry, dangerous for a balance
Leaderless (Cassandra, ScyllaDB, Riak). Any node accepts writes, and quorums tune consistency. With N replicas, R + W > N guarantees a read overlaps a node that saw the latest write. Supporting machinery from the Dynamo paper: read repair, anti-entropy with Merkle trees, hinted handoff, and version vectors or CRDTs to detect and merge concurrent writes. Reads and writes scale linearly, and the operational burden is real.
Consensus (Spanner, CockroachDB). Data is sharded into ranges, each running Paxos or Raft, with Spanner's TrueTime bounding clock uncertainty for external consistency. Both default to SERIALIZABLE, and CockroachDB targets about 4.5 second RTO with near-zero RPO. The price is written into physics: a write must reach a quorum, and if that quorum spans regions, every write pays a cross-region round trip. There is no way around it if you insist on strong consistency across distance.
What Harper changes
Every option so far treats the network between the application and the database as fixed, then works to hide it behind a replica, a cache, or a quorum. What if the network is the thing to remove?
Harper removes the network from between the application and the data, then lets you choose which data lives where.
One process, no internal hop. Database, cache, application runtime, and messaging (MQTT, WebSockets, SSE) run in a single in-memory Node.js process (GitHub). In a conventional stack, your logic makes a network call to the database even inside the same data center: serialize, cross the socket, wait, deserialize. In Harper, logic runs inside the storage process and calls the data directly.
// schema.graphqltype Product @table {
id: ID @primaryKey
name: String @indexed
price: Float
inventory: Int
}
// resources.jsexport class Product extends tables.Product {
static async get(target) {
const record = await super.get(target); // in-process read, no socket
return record ? { ...record, inStock: record.inventory > 0 } : record;
}
}
Where it sits in the taxonomy. Harper replication is peer-to-peer over WebSockets with gossip discovery and mTLS, and it is asynchronous with eventual consistency across nodes (docs). Within a node, operations are ACID with serializable isolation. Across nodes, conflict resolution is last-writer-wins by logical timestamp, with CRDT support since 4.3 for counters and independent property merges. In PACELC terms this is AP/EL, the same family as DynamoDB Global Tables by default, not the same family as Spanner. Harper does not give you global serializability, and you should say that out loud in your design review.
Selective replication is the cost and compliance lever. You can restrict replication per database, exclude tables with @table(replicate: false), set one-way flow with per-route sends and receives flags, cap copies with replicateTo, and compute per-record residency with setResidencyById. So a Frankfurt node holds only EU data, an APAC node only APAC data, and an analytics node subscribes to everything without publishing back. That cuts cross-region bandwidth and supports residency requirements, unlike a global table that copies everything everywhere.
The collapsed stack. A conventional global architecture needs a CDN, a cache tier, app servers, a database with replicas, a message broker, and cross-region sync tooling, each deployed and monitored in every region. Harper collapses the middle four into one process with replication built into the runtime. Fewer hops is the latency win. Fewer failure domains and vendors is often the bigger one.
Run the benchmark yourself
The claim above is easy to state and cheap to test. Deploy three Harper nodes in genuinely distant regions (us-east-1, eu-west-1, ap-southeast-1), then measure two read paths from each: the centralized path (all load generators target the node in region A) and the colocated path (each generator targets its local node).
Methodology that matters: discard 2,000 warm-up requests; use HTTP keep-alive and measure the TLS handshake separately with curl -w "tls=%{time_appconnect}s" rather than smearing it across samples; report p50, p95, and p99, never averages; and drive load open-loop. That last one is the subtle one. A closed-loop client stops sending when the server stalls, so it never records how bad the stall was. The fix is to schedule sends on a fixed cadence and measure latency from the intended send time.
// bench.mjs (core loop). node bench.mjs --url http://HOST:9926/Product/sku-1 --rate 500 --seconds 120
const interval = 1000 / rate;
const start = performance.now();
const inflight = [];
for (let i = 0; i < rate * seconds; i++) {
const intended = start + i * interval;
const now = performance.now();
if (intended > now) await new Promise((r) => setTimeout(r, intended - now));
// Do not await: fire on schedule regardless of server state.
inflight.push(request().then(() => {
samples[count++] = (performance.now() - intended) * 1000; // vs intended, not actual
}));
}
await Promise.all(inflight);
Expected magnitudes below are derived from published RTT figures, not measured Harper results. Replace them with your own numbers.
The flatness of the right column, not any single number in it, is the argument. Harper is not making the network faster. On the colocated path, the network is no longer in the read. The tradeoff you accept in return is that a local replica may briefly lag a write made in another region.
When to choose it, and when not to
The honest way to read the options is side by side, with the costs visible next to the wins.
Choose Harper's colocated model when global read latency dominates, your workload tolerates asynchronous convergence, and you want to cut both hops and operational surface area with per-table control over what lives where.
Do not choose it when you need strict global serializability (money movement, distributed locks, globally unique inventory decrements), when the same keys see heavy concurrent writes from multiple regions and every update must survive, or for heavy OLAP. Use Harper as the serving layer and feed a warehouse from it.
The distance between your users and your data is fixed, and no architecture on this page changes that. What stays a choice is how many times each request has to cross it, and how much of your operational surface you are willing to keep in every region to avoid the crossing.
The architecture

Run the benchmark yourself
The claim above is easy to state and cheap to test. Deploy three Harper nodes in genuinely distant regions (us-east-1, eu-west-1, ap-southeast-1), then measure two read paths from each: the centralized path (all load generators target the node in region A) and the colocated path (each generator targets its local node).
Methodology that matters: discard 2,000 warm-up requests; use HTTP keep-alive and measure the TLS handshake separately with curl -w "tls=%{time_appconnect}s" rather than smearing it across samples; report p50, p95, and p99, never averages; and drive load open-loop. That last one is the subtle one. A closed-loop client stops sending when the server stalls, so it never records how bad the stall was. The fix is to schedule sends on a fixed cadence and measure latency from the intended send time.
// bench.mjs (core loop). node bench.mjs --url http://HOST:9926/Product/sku-1 --rate 500 --seconds 120
const interval = 1000 / rate;
const start = performance.now();
const inflight = [];
for (let i = 0; i < rate * seconds; i++) {
const intended = start + i * interval;
const now = performance.now();
if (intended > now) await new Promise((r) => setTimeout(r, intended - now));
// Do not await: fire on schedule regardless of server state.
inflight.push(request().then(() => {
samples[count++] = (performance.now() - intended) * 1000; // vs intended, not actual
}));
}
await Promise.all(inflight);
Expected magnitudes below are derived from published RTT figures, not measured Harper results. Replace them with your own numbers.
The flatness of the right column, not any single number in it, is the argument. Harper is not making the network faster. On the colocated path, the network is no longer in the read. The tradeoff you accept in return is that a local replica may briefly lag a write made in another region
When to choose it, and when not to
The honest way to read the options is side by side, with the costs visible next to the wins.
Choose Harper's colocated model when global read latency dominates, your workload tolerates asynchronous convergence, and you want to cut both hops and operational surface area with per-table control over what lives where.
Do not choose it when you need strict global serializability (money movement, distributed locks, globally unique inventory decrements), when the same keys see heavy concurrent writes from multiple regions and every update must survive, or for heavy OLAP. Use Harper as the serving layer and feed a warehouse from it.
The distance between your users and your data is fixed, and no architecture on this page changes that. What stays a choice is how many times each request has to cross it, and how much of your operational surface you are willing to keep in every region to avoid the crossing.






.webp)


