---
title: 'Edge Locations'
source: 'https://academia.sh/en/courses/cloud-fundamentals/edge-locations'
course: 'Cloud Fundamentals'
language: en
updated: '2026-09-13T05:07:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Builds the user distribution and the candidate location set as a model and measures the average weighted distance'
    - 'Computes the latency gain per location and derives the point of diminishing returns: after the third location, the gain per unit falls below 0.2 ms'
    - 'Separates by name the work types the edge cannot serve and distinguishes the blended real latency drop from an edge-only count'
    - 'Converts cache lifetime into a daily stale-response count and the warmed content that does not carry at exit into origin load'
---

# Edge Locations

Counting proximity to the user together with its cost: how average distance and latency fall as edge locations are added, where the gain per location stops, the share of work types the edge cannot serve, and the daily count of stale responses returned from the edge.

The previous lesson read geography from the side of failure: wherever an item is placed,
what a loss takes and what the separation costs in units. The same geography's second
face exists even with no failure at all. The farther a subscriber is from the region, the
longer the path every request travels; this path is paid every day, on every request, and
it is never caught by any health check.

An **edge location** is a placement point independent of the source region that sits
close to the user and serves a portion of requests from there. What is rented here is not
a machine but a **set of locations**: which of the provider's operated points to use is
chosen, not what runs at those points. This lesson counts the payoff of proximity and the
work it does not cover, together.

## The Location Model

The setup below is a **model**. The fictional regional measurement network's subscribers
are distributed across a fictional plane; there is no real city, country, region, or
provider.

- **CG7.** The service geography is a fictional 1000×600-unit rectangle; the region sits
  at point (150, 500).
- **CG8.** 40 subscriber clusters are generated around five density hubs. The generator is
  self-written and the seed is visible as 20260802; every run gives the same numbers.
- **CG9.** Latency is linear in distance: 2.0 ms base + 0.075 ms per unit (fictional).
- **CG10.** An edge location's rent is 3.5 fictional `unit/hour`; the region's rent is
  outside this lesson's scope.
- **CG11.** Daily requests are 2,400,000, and the shares of the seven types are fixed.
- **CG12.** The hit rate in a warmed-up edge cache is 0.93; on the first day after a move,
  in a cold cache, 0.55.

```js
// edge/model.mjs — user distribution and location set is a MODEL. The fictional service
// geography is a 1000x600-unit rectangle; there is no real city, region, or provider.
export function generator(seed) {                // self-written linear congruential generator
  let s = seed >>> 0;
  return () => ((s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 4294967296);
}
export const SEED = 20260802;                    // visible seed; same numbers on every run

export const REGION = { name: "region", x: 150, y: 500 };
export const BASE_MS = 2.0, MS_PER_UNIT = 0.075;  // fictional: latency = base + distance * rate
export const LOCATION_RENT = 3.5;                 // fictional unit/hour, per edge location

export function subscribers() {                  // 5 density hubs, 8 clusters per hub
  const r = generator(SEED);
  const hub = Array.from({ length: 5 }, () => [r() * 1000, r() * 600]);
  const k = [];
  for (const [ox, oy] of hub)
    for (let i = 0; i < 8; i += 1)
      k.push({
        x: Math.min(1000, Math.max(0, ox + (r() + r() - 1) * 130)),
        y: Math.min(600, Math.max(0, oy + (r() + r() - 1) * 90)),
        weight: 1 + Math.floor(r() * 9),          // request share, 1..9
      });
  return k;
}

export function candidates() {                   // set of edge locations the provider offers
  const r = generator(SEED ^ 0x5f5f);
  return Array.from({ length: 14 }, (_, i) => ({ name: `k${i + 1}`, x: r() * 1000, y: r() * 600 }));
}

export const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
export const latency = (d) => BASE_MS + MS_PER_UNIT * d;

export function average(clusters, locations) {   // each cluster is served by the nearest location
  let weightSum = 0, sum = 0;
  for (const c of clusters) {
    sum += c.weight * Math.min(...locations.map((k) => distance(c, k)));
    weightSum += c.weight;
  }
  return sum / weightSum;
}

// request mix: types that can return from the edge and those that cannot (writes, reads that need consistency)
export const MIX = [
  ["static-shell", 0.28, "edge", 3600, 0.2],
  ["tariff-table", 0.10, "edge", 900, 2],
  ["subscriber-summary", 0.08, "edge", 60, 96],
  ["latest-read", 0.26, "consistent-read", 0, 0],
  ["read-upload", 0.18, "write", 0, 0],
  ["work-order-open", 0.06, "write", 0, 0],
  ["billing-close", 0.04, "write", 0, 0],
];
export const DAILY = 2_400_000, DAY_SEC = 86_400;
export const HIT_RATE = 0.93, COLD_HIT_RATE = 0.55;
```

## As Locations Are Added

Locations are added greedily: at each step, the candidate that lowers the average
weighted distance the most is chosen. At each step the gain and the rent are written down
together.

```js
// edge/placement.mjs — as locations are added, average distance, latency, and cost per location move together
import { REGION, subscribers, candidates, average, latency, LOCATION_RENT, SEED } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 2) => x.toFixed(n);
const cluster = subscribers(), candidate = candidates();
const chosen = [REGION], remaining = [...candidate];
const weight = cluster.reduce((a, c) => a + c.weight, 0);

console.log(`seed ${SEED}: ${cluster.length} subscriber clusters, total weight ${weight}, ` +
  `${candidate.length} candidate edge locations (fictional geography).`);
console.log();
console.log("location   | added   | avg. distance   | avg. latency   | gain ms   | rent   | ms per unit     | total rent  ");
console.log("-----------|---------|-----------------|----------------|-----------|--------|-----------------|-------------");

let previousL = latency(average(cluster, chosen));
console.log(`${s(0, 10)} | ${"-".padEnd(7)} | ${s(y(average(cluster, chosen)), 15)} | ` +
  `${s(y(previousL), 14)} | ${s("-", 9)} | ${s("-", 6)} | ${s("-", 15)} | ${s(y(0), 12)}`);

for (let n = 1; n <= 8; n += 1) {
  let best = null;
  for (const a of remaining) {
    const d = average(cluster, [...chosen, a]);
    if (best === null || d < best.d) best = { a, d };
  }
  chosen.push(best.a);
  remaining.splice(remaining.indexOf(best.a), 1);
  const l = latency(best.d), gain = previousL - l;
  console.log(`${s(n, 10)} | ${best.a.name.padEnd(7)} | ${s(y(best.d), 15)} | ${s(y(l), 14)} | ` +
    `${s(y(gain), 9)} | ${s(y(LOCATION_RENT), 6)} | ${s(y(gain / LOCATION_RENT), 15)} | ` +
    `${s(y(n * LOCATION_RENT), 12)}`);
  previousL = l;
}
```

```
seed 20260802: 40 subscriber clusters, total weight 210, 14 candidate edge locations (fictional geography).

location   | added   | avg. distance   | avg. latency   | gain ms   | rent   | ms per unit     | total rent  
-----------|---------|-----------------|----------------|-----------|--------|-----------------|-------------
         0 | -       |          338.60 |          27.39 |         - |      - |               - |         0.00
         1 | k2      |          215.03 |          18.13 |      9.27 |   3.50 |            2.65 |         3.50
         2 | k12     |          172.38 |          14.93 |      3.20 |   3.50 |            0.91 |         7.00
         3 | k13     |          146.26 |          12.97 |      1.96 |   3.50 |            0.56 |        10.50
         4 | k3      |          137.53 |          12.31 |      0.65 |   3.50 |            0.19 |        14.00
         5 | k7      |          134.13 |          12.06 |      0.26 |   3.50 |            0.07 |        17.50
         6 | k14     |          131.84 |          11.89 |      0.17 |   3.50 |            0.05 |        21.00
         7 | k1      |          130.96 |          11.82 |      0.07 |   3.50 |            0.02 |        24.50
         8 | k9      |          130.41 |          11.78 |      0.04 |   3.50 |            0.01 |        28.00
```

The first location alone lowers the average latency from 27.39 ms to 18.13 ms: for 3.5
units of rent, 9.27 ms, that is **2.65 ms per unit**. The second location brings 3.20 ms
for the same rent, the third 1.96 ms. At the fourth, the gain per unit falls to 0.19 ms —
a third of what the third location gave, a fourteenth of what the first gave. **The point
of diminishing returns is the third location.**

The harshness of the limit shows in the last rows. Three locations bring latency down to
12.97 ms for 10.50 units of rent; eight locations bring it to 11.78 ms for 28.00 units.
Five extra locations and 17.50 units gain a total of 1.19 ms. The curve flattens because,
as the location count grows, each new candidate approaches the leftovers of a set that is
already mostly served; if the user distribution is clustered around hubs, once the first
three hubs are taken there is no remaining density left to pick up.

## The Work the Edge Cannot Do

The table above assumes every request is served from the edge, and that assumption is
false. An edge location is a copy; it cannot accept a **write**, because the moment it
did, the record would diverge from the source. A **consistency-requiring read** cannot
return from the edge either: a subscriber asking for the reading they just uploaded
requires the source's most recent state.

```js
// edge/mix.mjs — work the edge cannot do, the blended real latency, and the staleness of an edge response
import { REGION, subscribers, candidates, average, latency, MIX, DAILY, DAY_SEC, HIT_RATE,
  COLD_HIT_RATE, LOCATION_RENT } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 2) => x.toFixed(n);
const cluster = subscribers(), candidate = candidates();
const chosen = [REGION];                            // the greedy choice's first three locations are rebuilt
for (let n = 0; n < 3; n += 1) {
  let e = null;
  for (const a of candidate)
    if (!chosen.includes(a)) {
      const d = average(cluster, [...chosen, a]);
      if (e === null || d < e.d) e = { a, d };
    }
  chosen.push(e.a);
}
const EDGE_MS = latency(average(cluster, chosen)), REGION_MS = latency(average(cluster, [REGION]));

console.log("type                 | share   | returns from edge   | TTL (s)   | daily change   | stale rate   | daily stale  ");
console.log("---------------------|---------|---------------------|-----------|----------------|--------------|--------------");
let staleTotal = 0, edgeShare = 0;
for (const [name, share, kind, ttl, change] of MIX) {
  const rate = kind === "edge" ? (change * (ttl / 2)) / DAY_SEC : 0;
  const stale = Math.round(share * DAILY * HIT_RATE * rate);
  staleTotal += stale; edgeShare += kind === "edge" ? share : 0;
  console.log(`${name.padEnd(20)} | ${s(y(share), 7)} | ${s(kind === "edge" ? "yes" : kind, 19)} | ` +
    `${s(ttl || "-", 9)} | ${s(change || "-", 14)} | ${s(rate ? `${y(100 * rate)}%` : "-", 12)} | ` +
    `${s(stale || "-", 13)}`);
}

const miss = EDGE_MS + REGION_MS;
const edgeBlend = HIT_RATE * EDGE_MS + (1 - HIT_RATE) * miss;
const blend = edgeShare * edgeBlend + (1 - edgeShare) * REGION_MS;
console.log();
console.log(`share returnable from the edge ${y(edgeShare)}; not returnable ${y(1 - edgeShare)} (write + consistent-read).`);
console.log(`latency: region ${y(REGION_MS)} ms, three-location edge ${y(EDGE_MS)} ms, ` +
  `on a miss ${y(miss)} ms, at hit rate ${HIT_RATE} the edge average is ${y(edgeBlend)} ms.`);
console.log(`blended real latency ${y(blend)} ms — a drop of ${y(REGION_MS - blend)} ms ` +
  `(${y((100 * (REGION_MS - blend)) / REGION_MS, 1)}%), whereas counting the edge alone would show ` +
  `${y((100 * (REGION_MS - EDGE_MS)) / REGION_MS, 1)}%.`);
console.log(`daily stale responses ${staleTotal.toLocaleString("en-US")} / ${DAILY.toLocaleString("en-US")} ` +
  `(${y((100 * staleTotal) / DAILY, 2)}%); the longest staleness is ${Math.max(...MIX.map((k) => k[3]))} s.`);

const warm = DAILY * (1 - edgeShare + edgeShare * (1 - HIT_RATE));
const cold = DAILY * (1 - edgeShare + edgeShare * (1 - COLD_HIT_RATE));
console.log(`requests reaching the origin: with a warmed-up cache ${Math.round(warm).toLocaleString("en-US")}, ` +
  `with a cold cache right after the move ${Math.round(cold).toLocaleString("en-US")} ` +
  `(+${y((100 * (cold - warm)) / warm, 1)}%); the rent for three locations is ${y(3 * LOCATION_RENT)} units.`);
```

```
type                 | share   | returns from edge   | TTL (s)   | daily change   | stale rate   | daily stale  
---------------------|---------|---------------------|-----------|----------------|--------------|--------------
static-shell         |    0.28 |                 yes |      3600 |            0.2 |        0.42% |          2604
tariff-table         |    0.10 |                 yes |       900 |              2 |        1.04% |          2325
subscriber-summary   |    0.08 |                 yes |        60 |             96 |        3.33% |          5952
latest-read          |    0.26 |     consistent-read |         - |              - |            - |             -
read-upload          |    0.18 |               write |         - |              - |            - |             -
work-order-open      |    0.06 |               write |         - |              - |            - |             -
billing-close        |    0.04 |               write |         - |              - |            - |             -

share returnable from the edge 0.46; not returnable 0.54 (write + consistent-read).
latency: region 27.39 ms, three-location edge 12.97 ms, on a miss 40.36 ms, at hit rate 0.93 the edge average is 14.89 ms.
blended real latency 21.64 ms — a drop of 5.75 ms (21.0%), whereas counting the edge alone would show 52.7%.
daily stale responses 10,881 / 2,400,000 (0.45%); the longest staleness is 3600 s.
requests reaching the origin: with a warmed-up cache 1,373,280, with a cold cache right after the move 1,792,800 (+30.5%); the rent for three locations is 10.50 units.
```

The share returnable from the edge is **0.46**; the remaining 0.54 is writes and
consistency-requiring reads, and it goes to the region no matter how many locations
exist. This is why the blended real latency stays at 21.64 ms and the drop is **21.0
percent** — a measurement that looked only at the edge column would have reported 52.7
percent. The gap between the two numbers is not a measurement error; it is the answer to
the question of which work can return from the edge.

The miss row carries a second warning. A request not found in the cache goes to the edge
and then on to the source, so it climbs to 40.36 ms — longer than going directly to the
region. An edge location does not speed up every request; it speeds up the ones it
**hits**, and slows down the ones it misses.

## Staleness and What Carries at Exit

A response returned from the edge is, by definition, a copy, and a copy has an age. The
measure is the product of cache lifetime and how often the data changes: the subscriber
summary, with a 60-second lifetime, changes 96 times a day, and **3.33 percent** of its
requests return stale; the tariff table, at 900 seconds and 2 changes a day, is 1.04
percent; the static shell, at 3600 seconds but only 0.2 changes a day, is 0.42 percent.
The ranking is not the ranking of cache lifetime; it is the ranking of the **product** of
lifetime and change rate.

In total, **10,881 stale responses** return each day. This number, 0.45 percent of all
requests, is the number that has to be written down before a decision can be made about
whether it is acceptable; the longest staleness is 3600 seconds, which means a subscriber
whose tariff just changed can see a tariff up to an hour old.

The decision handed over here is three items: which locations are operated, which
location a request is routed to, and when an entry is evicted from the cache. What is
received in return is a 5.75 ms blended drop for 10.50 units of rent, and requests
reaching the origin falling from 2,400,000 to 1,373,280. What carries at exit is only the
**rules**: for the three types, the cache lifetime, the key, and the invalidation
definition can be written down and built again unchanged. Two things do not carry: the
provider's location set and the **warmed-up content**. With a cold cache, requests
reaching the origin on the first day climb to 1,792,800, a rise of **30.5 percent**; the
cost of the move, paid once, is exactly this difference.

## Summary

- An edge location is renting a set of locations: which points to use is chosen, not what
  runs at those points.
- Average latency falls from 27.39 ms to 18.13 ms at the first location; the gain per unit
  drops from 2.65 ms to 0.56 ms at the third location and to 0.19 ms at the fourth — **the
  point of diminishing returns is the third location**.
- Three locations give 12.97 ms for 10.50 units, while eight locations give 11.78 ms for
  28.00 units: an extra 17.50 units gains a total of 1.19 ms.
- The share that cannot return from the edge is 0.54 (writes and consistency-requiring
  reads); the blended real latency drop is 21.0 percent, whereas counting the edge alone
  would show 52.7 percent. A missed request, at 40.36 ms, is slower than going directly to
  the region.
- Staleness is the product of lifetime and change rate: 3.33 percent for the subscriber
  summary, 1.04 percent for the tariff table, 0.42 percent for the static shell; 10,881
  stale responses a day and the longest staleness is 3600 seconds.
- The cache rules carry at exit; the location set and the warmed-up content do not: on a
  cold start, requests reaching the origin rise by 30.5 percent.

## Next Step

Two lessons measured two axes separately, and the two pull against each other. Spreading
across three zones raised resilience and slowed the synchronous write. Three edge
locations lowered latency and produced 10,881 stale responses a day and 10.50 units of
rent. Every decision has a gain column and at least one loss column, but until now these
columns have been counted lesson by lesson and scattered. There is an arrangement that
holds decisions in a single table, scores each one against the same five measures, and
makes visible which measure conflicts with which. The next lesson defines those five
columns as types and scores a decision set against them, counting the tension across
columns: how many decisions that raise one column lower another, and which column no
decision improves.
