Skip to content
academia.sh

Lesson 07 / 10

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.

Published Updated

In this lesson

  • 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
Contents

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.
// 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.

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close