Lesson 13 / 16
Gateway Offloading
Turning the move of common work to the edge into a mechanical rule: separating what is movable once a check's inputs are written down, measuring the repeated line count, the cost of the trust boundary shifting, and reading which rate a check moved to the edge runs at from the K01 calculation.
Contents
Aggregation was the gateway’s job that touched the body. Most work that can move to the edge never touches the body at all: verifying a token’s signature, applying a rate limit, writing an access log entry. Gateway offloading takes this work off the services and gathers it in one place at the edge.
The first lesson compared this layout by line count but did not turn movability into a rule, and left “work that looks at the request’s metadata belongs at the edge” as a statement, not a rule. This lesson makes that criterion mechanical: every check’s inputs get written down, and the inputs’ class separates the check on its own. Token validation was built in the Authentication and Authorization course, rate limiting in the Web API Design course, TLS termination in this course’s own Role of the Load Balancer lesson; none of it is retold here.
The Portability Rule
A check can read two kinds of input. Metadata comes from the request itself: headers, path, source address, content length. Domain input comes from the system’s data: a shipment record, a contract record, a daily counter. The rule is one sentence: a check that reads only metadata can move to the edge; a check that reads a domain input stays at the service.
The rule’s reasoning is a coupling count. When a check reading domain input moves to the edge, the gateway is forced to reach that data; as the first lesson said, the edge starts knowing both contexts’ domain rules at once, and every change to either context forces a redeploy. The rule is the measurable form of refusing to build that coupling.
// edge/check.mjs — six checks, the inputs they read, and the portability rule export const METADATA = ["headers", "path", "source address", "content length"]; // reads: the decision's required inputs; optional: input requested for the log but not decision-changing export const CHECK = { "token signature": { reads: ["headers"], optional: [], lines: 12 }, "rate limit": { reads: ["source address", "headers"], optional: [], lines: 9 }, "body size": { reads: ["content length"], optional: [], lines: 5 }, "access log": { reads: ["path", "headers"], optional: ["shipment record"], lines: 6 }, "shipment ownership": { reads: ["headers", "shipment record"], optional: [], lines: 14 }, "contract quota": { reads: ["contract record", "daily counter"], optional: [], lines: 11 }, }; export const isMetadata = (g) => METADATA.includes(g); export const movable = (d) => d.reads.every(isMetadata); export const missingFields = (d) => (movable(d) ? d.optional.filter((g) => !isMetadata(g)).length : 0);
The line counts are the model’s input and belong to the assumption class; what is read is not a single implementation’s length but the difference the same lines repeating across three services produces. The rule, in turn, follows from the class of the inputs and is independent of the line counts.
// edge/offload.mjs — applies the rule to six checks, counts lines, and ties them to the K01 calculation import { CHECK, movable, missingFields, isMetadata } from "./check.mjs"; const SERVICE_COUNT = 3; // tracking, event, fee const name = Object.keys(CHECK); const edge = name.filter((a) => movable(CHECK[a])); const service = name.filter((a) => !movable(CHECK[a])); console.log(`${"check".padEnd(19)}${"domain input".padStart(13)} decision`); for (const a of name) { const fields = CHECK[a].reads.filter((g) => !isMetadata(g)); console.log(`${a.padEnd(19)}${String(fields.length).padStart(13)} ` + `${movable(CHECK[a]) ? "moves to edge" : `stays at service (${fields.join(", ")})`}`); } const missing = edge.reduce((t, a) => t + missingFields(CHECK[a]), 0); console.log(`\nmoved to edge = ${edge.length}/${name.length}, stays at service = ${service.length}/${name.length}`); console.log(`record fields missing at edge = ${missing}`); const lines = (list) => list.reduce((t, a) => t + CHECK[a].lines, 0); const LAYOUT = [ ["at each service", [], name], ["offloaded", edge, service], ]; console.log(`\n${"layout".padEnd(14)}${"edge lines".padStart(14)}${"service lines".padStart(15)}${"total".padStart(9)}`); for (const [y, k, s] of LAYOUT) { console.log(`${y.padEnd(14)}${String(lines(k)).padStart(14)}` + `${String(SERVICE_COUNT * lines(s)).padStart(15)}${String(lines(k) + SERVICE_COUNT * lines(s)).padStart(9)}`); } // K01 Back-of-the-Envelope Estimation: peak edge 513.89 requests/s (computed-value class) const EDGE_PEAK = 513.89; const TRUST = [ ["no gateway", SERVICE_COUNT, 1, 0], ["gateway validates, service trusts", 1, 1, SERVICE_COUNT], ["gateway and service validate", 1 + SERVICE_COUNT, 2, 0], ]; console.log(`\n${"token validation".padEnd(34)}${"units".padStart(7)}${"validations/s".padStart(15)}${"unprotected service".padStart(20)}`); for (const [y, units, times, open] of TRUST) { console.log(`${y.padEnd(34)}${String(units).padStart(7)}` + `${(EDGE_PEAK * times).toFixed(2).padStart(15)}${`${open}/${SERVICE_COUNT}`.padStart(20)}`); } // The rate a check runs at: K01's three-flow calculation const RATE = [ [`edge checks (${edge.length})`, 513.89, "requests/s", "peak at the edge"], ["shipment ownership", 416.67, "requests/s", "read peak"], ["contract quota", 833.33, "records/s", "batch job scan rate"], ]; console.log(""); for (const [a, v, u, k] of RATE) console.log(`${a.padEnd(23)}${v.toFixed(2).padStart(9)} ${u.padEnd(11)} ${k}`);
check domain input decision token signature 0 moves to edge rate limit 0 moves to edge body size 0 moves to edge access log 0 moves to edge shipment ownership 1 stays at service (shipment record) contract quota 2 stays at service (contract record, daily counter) moved to edge = 4/6, stays at service = 2/6 record fields missing at edge = 1 layout edge lines service lines total at each service 0 171 171 offloaded 32 75 107 token validation units validations/s unprotected service no gateway 3 513.89 0/3 gateway validates, service trusts 1 513.89 3/3 gateway and service validate 4 1027.78 0/3 edge checks (4) 513.89 requests/s peak at the edge shipment ownership 416.67 requests/s read peak contract quota 833.33 records/s batch job scan rate
The Distinction the Rule Produces
Four of the six checks move to the edge, two stay at the service, and what makes the distinction
is not a judgment call but a list. shipment ownership asks whether a shipment belongs to the
identity making the request; answering requires reading the shipment record, so it cannot be
given at the edge. This is the edge counterpart of object-level authorization from the
Authentication and Authorization course: verifying identity can be done at the edge, deciding
whether that identity can access a specific object cannot. contract quota stays at the service
even more firmly, since it reads two domain inputs.
The access log row shows the rule’s boundary. The check moves to the edge because it only reads
metadata, but its optional input is a domain input: the record written at the edge knows which
path was called, not which shipment the call returned. The counter writes this as record fields missing at edge = 1. Offloading here does not move the work — it drops one of the work’s fields;
the decision’s written form should read “the access log moved to the edge, the record is missing
one field.”
The line table gives the gain: the six checks take 171 lines across three services, while the offloaded layout takes 32 lines at the edge and 75 at the services, 107 total. The total falls to 0.63 times. The drop does not reach a third because the two checks that stay at the service still repeat across three services — and those two are the longest two.
Where the Trust Boundary Shifts
Offloading’s real cost is not in the lines, it is in the second table. Without a gateway, all three services do their own token validation: code sits in three units, validation runs 513.89 times a second, unprotected service is 0/3. Once validation moves to the edge and the services trust the gateway, code drops to one unit and the validation count does not change, but unprotected service becomes 3/3: a request bypassing the gateway is validated by none of the three services. The gain in code lines was paid for with a trust assumption, and the decision is incomplete until that assumption is written down.
The third row removes the assumption: validating at both the gateway and the service returns unprotected service to 0/3, but code rises to four units and validation runs 1027.78 times a second — the measure of doing the same work twice. None of the three rows leads on every measure; the choice is written down together with whether the gateway can be bypassed.
Back to the Calculation
The last table uses the course’s rule in both directions. The four checks moved to the edge run at K01’s peak edge rate: 513.89 requests a second, the system’s highest request rate. A check gathered in one place at the edge costs that check multiplied by 513.89, and offloading does not lower this multiplier — it only gathers it in one place.
The two checks that stay at the service run at different rates. shipment ownership belongs to
the read flow and runs at K01’s peak read rate, 416.67 requests a second. contract quota belongs
to the batch flow and runs at K01’s end-of-day scan rate, 833.33 records a second. The result is
the opposite of what might be expected: a check that does not move to the edge can run faster than
one that does. Offloading is not a performance decision, it is a placement decision; the most
expensive work does not have to be at the edge.
Summary
- A check that reads only metadata can move to the edge; one that reads a domain input stays at the service — the rule follows mechanically from the list of inputs.
- Four of six checks move; two stay:
shipment ownershipreads the shipment record,contract quotareads the contract record and daily counter. - Offloading can drop one of a check’s fields: the access log written at the edge is missing one
field (
record fields missing at edge = 1). - Line count falls from 171 to 107, 0.63 times; the limit is that the two checks staying at the service still repeat across three services.
- The trust boundary shifts: moving validation to the edge drops code from 3 units to 1 but raises unprotected service from 0/3 to 3/3; validating in both places returns it to 0/3, at a cost of 4 units and validation rising from 513.89 to 1027.78 times a second.
- Back to K01: edge checks run at the peak edge rate (513.89 requests/s), while the two checks staying at the service run at 416.67 requests/s and 833.33 records/s — the edge is not where the most expensive work has to be.
Next Step
The three lessons so far treated the gateway as a single edge: every client arrives at the same address, passes the same checks, and receives the same merged body when aggregation runs. But the clients asking for tracking information are not the same. A mobile tracking card shows the state and zone, a web tracking page also opens the route steps, a seller panel wants the fee line items. When one merged body serves all three, every client also carries fields it does not use. The next lesson counts these fields and measures how many bytes splitting the edge by client type cuts from the body, and how many edge units it adds in exchange.
To keep your progress and take notes, Log in
My notes
Log in to take notes.