---
title: 'Edge Limits'
source: 'https://academia.sh/en/courses/serverless/edge-limits'
course: 'Serverless and Edge Computing'
language: en
updated: '2026-09-13T05:07:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Applies three thresholds to a job set and counts how many jobs hit which threshold'
    - 'Measures the cost of splitting and falling back to the center in extra trips, extra time, and failure points'
    - 'Counts three cases where the edge does not pay off and compares three tiers on the same job'
---

# Edge Limits

The edge's three thresholds are applied to a set of twelve jobs; the cost of splitting is worked out in extra trips and failure points, and the three cases where the edge does not pay off are counted. The course wrap-up sums the delegated decision and the constraint of twenty lessons.

The previous lesson made resource access declarative. One thing was left untested: **how much
work can actually be done at the edge**. Requests were processed, state was held, a queue was set
up, a resource was bound — all of it assumed to fit at the edge. This lesson applies three
thresholds to a job set and works out the bill for fitting what does not fit.

The measurement runs on the fictional **measurement network** and is a **model**: the
thresholds, the job set, and the user groups are set up with `node`, and prices are a fictional
unit.

**EC37.** The three thresholds are fictional: 50 ms of processing per request, 128 MB of memory,
and the missing interface items counted in the first lesson.
**EC38.** The job set has twelve jobs; for each one, processing time, peak memory, and requested
items are given.
**EC39.** A split job uses 80% of the threshold; between parts, intermediate state is written to
the store (64 ms) and read (2 ms). A failure point is the step count plus the transition count.
**EC40.** A job that hits compatibility cannot be split; it falls back to the center, and the edge
only routes it.
**EC41.** For six user groups, the request round trip is given both for ending at the edge and for
going straight to the center; a request that passes through the edge on its way to the center pays
the sum of the two.
**EC42.** The standing server does the same job at a 0.55 factor; the function's cold rate is 2%,
its cold extra is 900 ms.

## Three Thresholds, Twelve Jobs

```js
// limit.mjs — the edge runtime's three thresholds and the fictional job set (model).
export const THRESHOLD = { time: 50, memory: 128 };  // ms processing per request, MB memory
export const MISSING = ["file system", "local disk", "long-running process", "raw socket", "native binary addon"];
export const JOBS = [                                // processing ms, memory MB, requested missing items
  { name: "request routing", time: 3, memory: 6 },
  { name: "header editing", time: 2, memory: 4 },
  { name: "session validation", time: 9, memory: 12 },
  { name: "tariff lookup", time: 6, memory: 22 },
  { name: "read-format conversion", time: 18, memory: 40 },
  { name: "counter signature validation", time: 34, memory: 48 },
  { name: "anomaly threshold", time: 62, memory: 90 },
  { name: "consumption curve", time: 140, memory: 260 },
  { name: "log archiving", time: 40, memory: 96, requires: ["file system", "local disk"] },
  { name: "invoice document", time: 220, memory: 180, requires: ["file system", "native binary addon"] },
  { name: "nightly reconciliation", time: 40000, memory: 700, requires: ["long-running process", "file system"] },
  { name: "persistent device connection", time: 5, memory: 30, requires: ["raw socket", "long-running process"] },
];
export const SHARE = 0.8;                            // share of the threshold a part may use
export const PART_FIXED = 20;                        // fixed memory per part, MB
export const WRITE = 64, READ = 2, FALLBACK = 60;    // store write/read, edge-region trip, ms

export function check(i) {                           // the three thresholds are applied separately
  const exceeded = [];
  if (i.time > THRESHOLD.time) exceeded.push("time");
  if (i.memory > THRESHOLD.memory) exceeded.push("memory");
  const missing = (i.requires ?? []).filter((k) => MISSING.includes(k));
  if (missing.length) exceeded.push("compatibility");
  return { ...i, exceeded, missing };
}
export function split(i) {                           // only a job exceeding time/memory can be split
  const usable = THRESHOLD.time * SHARE;
  const parts = Math.ceil(i.time / usable);
  const peakMemory = Math.ceil(i.memory / parts) + PART_FIXED;
  return { parts, peakMemory, extraTrips: (parts - 1) * 2, extraTime: (parts - 1) * (WRITE + READ),
    failurePoints: 2 * parts - 1 };
}
export const row = (...s) => console.log(s.map((x, i) => String(x).padEnd(i ? 15 : 33)).join("").trimEnd());
```

```js
// threshold.mjs — the three thresholds applied to the job set; how many jobs run at the edge and what splitting costs.
import { THRESHOLD, MISSING, JOBS, SHARE, check, split, FALLBACK, row } from "./limit.mjs";

console.log(`thresholds: time ${THRESHOLD.time} ms/request, memory ${THRESHOLD.memory} MB, ` +
  `interface items missing at the edge ${MISSING.length}\n`);
row("job", "time", "memory", "missing items", "result");
const d = JOBS.map(check);
for (const i of d) row(i.name, `${i.time} ms`, `${i.memory} MB`, i.missing.length,
  i.exceeded.length ? `${i.exceeded.join(" + ")} exceeded` : "runs");
const countBy = (e) => d.filter((i) => i.exceeded.includes(e)).length;
const running = d.filter((i) => !i.exceeded.length);
console.log(`\n${running.length} of ${JOBS.length} jobs run at the edge; ` +
  `time stops ${countBy("time")}, memory stops ${countBy("memory")}, compatibility stops ${countBy("compatibility")} ` +
  `(a job can hit more than one threshold)\n`);

const splittable = d.filter((i) => i.exceeded.length && !i.exceeded.includes("compatibility"));
const fallenBack = d.filter((i) => i.exceeded.includes("compatibility"));
row("split job", "parts", "peak memory", "extra trips", "extra time", "failure points");
let extraTrips = 0, extraTime = 0, failurePoints = 0;
for (const i of splittable) {
  const p = split(i);
  extraTrips += p.extraTrips; extraTime += p.extraTime; failurePoints += p.failurePoints;
  row(i.name, p.parts, `${p.peakMemory} MB`, p.extraTrips, `${p.extraTime} ms`, p.failurePoints);
}
console.log(`\nsplitting brings ${splittable.length} jobs into the edge: threshold share ${SHARE * 100}%, ` +
  `total ${extraTrips} extra trips, ${extraTime} ms extra time, ${failurePoints} failure points (1 per job before splitting)`);
console.log(`the ${fallenBack.length} jobs that cannot be split fall back to the center (${fallenBack.map((i) => i.name).join(", ")}); ` +
  `the cost is a ${FALLBACK} ms edge-region trip per request, the edge only routes`);
console.log(`final state: ${running.length + splittable.length}/${JOBS.length} at the edge, ` +
  `${fallenBack.length}/${JOBS.length} at the center`);
```

```
thresholds: time 50 ms/request, memory 128 MB, interface items missing at the edge 5

job                              time           memory         missing items  result
request routing                  3 ms           6 MB           0              runs
header editing                   2 ms           4 MB           0              runs
session validation               9 ms           12 MB          0              runs
tariff lookup                    6 ms           22 MB          0              runs
read-format conversion           18 ms          40 MB          0              runs
counter signature validation     34 ms          48 MB          0              runs
anomaly threshold                62 ms          90 MB          0              time exceeded
consumption curve                140 ms         260 MB         0              time + memory exceeded
log archiving                    40 ms          96 MB          2              compatibility exceeded
invoice document                 220 ms         180 MB         2              time + memory + compatibility exceeded
nightly reconciliation           40000 ms       700 MB         2              time + memory + compatibility exceeded
persistent device connection     5 ms           30 MB          2              compatibility exceeded

6 of 12 jobs run at the edge; time stops 4, memory stops 3, compatibility stops 4 (a job can hit more than one threshold)

split job                        parts          peak memory    extra trips    extra time     failure points
anomaly threshold                2              65 MB          2              66 ms          3
consumption curve                4              85 MB          6              198 ms         7

splitting brings 2 jobs into the edge: threshold share 80%, total 8 extra trips, 264 ms extra time, 10 failure points (1 per job before splitting)
the 4 jobs that cannot be split fall back to the center (log archiving, invoice document, nightly reconciliation, persistent device connection); the cost is a 60 ms edge-region trip per request, the edge only routes
final state: 8/12 at the edge, 4/12 at the center
```

The three thresholds pass six of the twelve jobs; among the remaining six, time stops four,
memory stops three, compatibility stops four, and two jobs hit all three at once.

The second table writes down what splitting buys. Two jobs that hit time and memory can be split,
and splitting divides memory too: the consumption curve's peak memory drops from 260 MB to 85 MB.
The cost is eight extra trips, 264 ms, and **ten failure points** — before splitting, each job was
a single point. The four jobs that hit compatibility cannot be split, because what is missing is
not time, it is an interface item.

## Where the Edge Does Not Pay Off

```js
// tier.mjs — where the edge does not pay off, and the same job compared across three tiers.
import { JOBS, split, row } from "./limit.mjs";

const GROUP = [                                      // trip durations ms, model input
  { name: "branch-a", requests: 2600, atEdge: 8, direct: 124 },
  { name: "branch-b", requests: 2100, atEdge: 12, direct: 116 },
  { name: "branch-c", requests: 1800, atEdge: 18, direct: 80 },
  { name: "center-campus", requests: 1500, atEdge: 22, direct: 14 },
  { name: "in-datacenter", requests: 1200, atEdge: 28, direct: 6 },
  { name: "branch-d", requests: 800, atEdge: 14, direct: 132 },
];   // a request going to the center passes through the edge, so direct adds the edge's own trip
const N = GROUP.reduce((t, g) => t + g.requests, 0);
const avg = (f) => GROUP.reduce((t, g) => t + g.requests * f(g), 0) / N;

const losing = GROUP.filter((g) => g.direct <= g.atEdge);
const kn = losing.reduce((t, g) => t + g.requests, 0);
console.log(`${N} requests, ${GROUP.length} groups: weighted gain for a request ending at the edge ` +
  `${avg((g) => g.direct - g.atEdge).toFixed(1)} ms, loss for a request going to the center ` +
  `${avg((g) => g.atEdge).toFixed(1)} ms`);
console.log(`${losing.length}/${GROUP.length} groups (${kn} requests, ${(100 * kn / N).toFixed(0)}%) ` +
  `are already close to the resource: the edge charges them ${losing.map((g) => g.direct - g.atEdge).join(" and ")} ms\n`);

const WRITE = 64, REGION_WRITE = 10, SERVER_FACTOR = 0.55;
const edgeWrite = avg((g) => g.atEdge) + WRITE, regionWrite = avg((g) => g.direct) + REGION_WRITE;
console.log(`consistent write: from the edge ${edgeWrite.toFixed(1)} ms, from the region ${regionWrite.toFixed(1)} ms; gain ` +
  `${(regionWrite - edgeWrite).toFixed(1)} ms, ${(100 * (regionWrite - edgeWrite) / avg((g) => g.direct - g.atEdge)).toFixed(0)}% ` +
  `of the edge-terminated gain — the ${WRITE} ms never comes off`);
const heavy = JOBS.find((i) => i.name === "consumption curve"), heavyParts = split(heavy), serverMs = heavy.time * SERVER_FACTOR;
console.log(`heavy computation (${heavy.time} ms): at the edge in ${heavyParts.parts} parts ${heavy.time + heavyParts.extraTime} ms, ` +
  `on the server ${Math.round(serverMs)} ms — ${((heavy.time + heavyParts.extraTime) / serverMs).toFixed(1)}x\n`);

const JOB = JOBS.find((i) => i.name === "read-format conversion");
const MONTHLY = 12_000_000, COLD_RATE = 0.02, COLD_EXTRA = 900;
const RATE = { edge: 0.0000003, func: 0.0000012, serverHour: 0.096 };   // fictional unit
const concurrent = Math.ceil((MONTHLY / 2_592_000) * (JOB.time / 1000));
const edgeAvg = avg((g) => g.atEdge), regionAvg = avg((g) => g.direct);
const TIER = [
  ["edge runtime", edgeAvg + JOB.time, "50 ms / 128 MB / 5 missing items", MONTHLY * RATE.edge],
  ["function execution environment", regionAvg + JOB.time + COLD_RATE * COLD_EXTRA, "900 s / 3008 MB / cold",
    MONTHLY * RATE.func],
  ["standing server", regionAvg + JOB.time * SERVER_FACTOR, "capacity upfront / scaling by hand",
    Math.max(1, Math.ceil(concurrent / 40)) * 730 * RATE.serverHour],
];
console.log(`the same job "${JOB.name}", ${MONTHLY / 1e6} million requests a month; units are fictional`);
row("tier", "latency", "monthly units", "constraint");
for (const [name, ms, constraint, units] of TIER)
  row(name, `${ms.toFixed(1)} ms`, units.toFixed(2), constraint);
const [e, f, s] = TIER.map((x) => x[3]);
console.log(`the edge is ${(f / e).toFixed(1)}x cheaper than the function, ${(s / e).toFixed(0)}x cheaper than the server; ` +
  `but this row can only be written for the ${JOBS.length - 4}/${JOBS.length} jobs that fit the three thresholds`);
```

```
10000 requests, 6 groups: weighted gain for a request ending at the edge 68.8 ms, loss for a request going to the center 15.6 ms
2/6 groups (2700 requests, 27%) are already close to the resource: the edge charges them -8 and -22 ms

consistent write: from the edge 79.6 ms, from the region 94.4 ms; gain 14.8 ms, 21% of the edge-terminated gain — the 64 ms never comes off
heavy computation (140 ms): at the edge in 4 parts 338 ms, on the server 77 ms — 4.4x

the same job "read-format conversion", 12 million requests a month; units are fictional
tier                             latency        monthly units  constraint
edge runtime                     33.6 ms        3.60           50 ms / 128 MB / 5 missing items
function execution environment   120.4 ms       14.40          900 s / 3008 MB / cold
standing server                  94.3 ms        70.08          capacity upfront / scaling by hand
the edge is 4.0x cheaper than the function, 19x cheaper than the server; but this row can only be written for the 8/12 jobs that fit the three thresholds
```

Three cases count where the edge does not pay off. Two of the six groups — 2,700 requests, 27% —
are closer to the region than to the edge, and the edge **charges** them 8 to 22 ms. In consistent
writes, because ordering sits in the center, the 64 ms never comes off: the gain drops from 68.8
to 14.8 ms. In heavy computation, a 140 ms job costs 338 ms at the edge, 77 ms on the server.

The last table sets three tiers side by side on the same job. For a job that fits the thresholds,
the edge wins on all three measures: 33.6 ms and 3.60 fictional units a month — a quarter of the
function's cost, a nineteenth of the server's. The condition is in the last sentence: **it can
only be written for eight of the twelve jobs.**

## Summary

- The three thresholds passed 6 of the twelve jobs; time stopped 4, memory stopped 3,
  compatibility stopped 4.
- Splitting brought 2 more jobs into the edge and dropped peak memory from 260 MB to 85 MB; the
  cost is 8 extra trips, 264 ms, and 10 total failure points instead of 1 per job. The 4 jobs that
  hit compatibility cannot be split and fall back to the center: 8/12 at the edge, 4/12 at the
  center.
- The edge does not pay off in three places: 2,700 requests (27%) are already close to the region,
  the consistent-write gain drops from 68.8 to 14.8 ms, and a 140 ms computation takes 338 ms at
  the edge against 77 ms on the server.
- For a job that fits, the edge is 4 times cheaper than the function and 19 times cheaper than the
  server at 33.6 ms and 3.60 units — but only for 8/12 of the set.

## Course Wrap-Up

| Lesson | Decision handed over | Constraint that comes back | Cost of working around it |
|---|---|---|---|
| FC-01 Serverless concept | capacity, 8 decisions | concurrency 200 | 11,520 idle instance-minutes |
| FC-02 Invocation models | retries | 5,487 colliding records | deduplication 5.5% |
| FC-03 Cold start | pool lifetime | cold call 1,300 ms | warming 50% of the bill |
| FC-04 Resource limits | memory tier | 6 of 12 do not fit | 246 calls, 245 records |
| FC-05 Aliases | traffic split | +100 ms at a 1% share | 15 synthetic calls |
| FC-06 Shared layers | dependency location | cold-start floor 745 → 2,093 | 6 republishes |
| FC-07 Jobs that do not fit | standing capacity | 2 of 5 do not fit | 13 failure points |
| ED-01 Event bus | routing | 433 silent drops | 781 wasted wake-ups |
| ED-02 Queue and topic | consumer scaling | 5 tunable items | 1 message, 7 requests |
| ED-03 Orchestration | flow state | 1,119 transitions, 280 units | 840 units if split into three |
| ED-04 Scheduled triggering | the clock | 3,600 duplicate invoices | a scope query per run |
| ED-05 Gateway service | gateway operation | 102 configuration items | 48,469 re-invocations |
| ED-06 Notification services | sending | 25-day throttling | 630 events, 6.3 units |
| EC-01 Runtime | setup and version | 7 of 13 items missing | +9 round trips per request |
| EC-02 Request processing | placement | 3 of 7 do not run at the edge | +64 ms, 640 s |
| EC-03 Edge storage | replication | 12,857 stale reads | read 2 → 62 ms |
| EC-04 Durable state objects | concurrency | cap 166.7 req/s | key split, 6 reads |
| EC-05 Edge queues | asynchronous work | 438 wrong index | 2,400 extra writes |
| EC-06 Bindings | connection setup | 5 of 9 types | 45 lines of code, 124 ms |
| EC-07 Edge limits | processing and memory | 6 of 12 fit | 8 extra trips, 10 failure points |

The course's rule is the same across all twenty rows: **handing over capacity management does
not eliminate it; it comes back as a constraint, and that constraint decides the design.** The
third column falls into four families: in eight rows the constraint is **a numbered upper bound**
(concurrency, duration, memory, interface items, requests/s), in three rows it is **a startup
cost**, in four rows it is **an invisible configuration surface** (a rule, five items, a flow
definition, 102 items), in five rows it is **silent loss or duplicated work**. The fourth column,
in turn, is never written in the constraint's own unit: a duration limit turns into a call count,
a retention period into a second copy, a missing interface into a network round trip.

Four courses in a row, every layer got rented: the machine, the network, the data, and computation
itself. Every rental handed over a decision and returned a countable constraint in exchange. But
who all these resources **belong to**, who can access what, and whose bill it lands on was never
asked. **Cloud Governance, Security and Cost** starts with that question.
