Lesson 16 / 16
Gatekeeper Pattern
Isolating validation into a separate layer: separating checks that can be given at the gate from those that must stay at the application, measuring the rate and bytes at which invalid requests reach the application, reducing the number of units touching untrusted input, and applying the result to K01's peak edge rate.
Contents
Across five lessons, every piece of work placed at the edge was written down with a gain and a cost, but all of them shared one assumption: a request reaching the edge is a request worth processing. Part of the peak edge rate, though, is made of requests that will never be answered — unrecognized paths, bodies with a required field missing, oversized bodies, malformed tokens.
The gatekeeper is a layout that isolates validation into a separate layer: only this layer sees untrusted input, and the application receives only a validated, normalized request. What separates it from the gateway is the scope of responsibility; the gateway routes, aggregates, and does offloaded work, while the gatekeeper asks a single question: should this request reach the application. This lesson measures how many requests get “no” as the answer to that question.
Validation the Gate Can Give
The third lesson’s portability rule applies here too: a check that looks only at the request itself can be given at the gate, one that looks at domain data cannot. The model separates this into two check sets and runs it over a deterministic set of sixty requests. The set is constructed: each of six flaw types occurs five times, because what is meant to be measured is not a realistic error rate but which of the flaws can be held at the gate.
// gate/request.mjs — deterministic request set and two validation layers; in-process model export const SHIPMENT = { "TR-4821": { owner: "M7" }, "TR-9007": { owner: "M3" } }; const BASE = { path: "/tracking", trackingNo: "TR-4821", token: "b.M7.sig", bodyBytes: 120 }; // One flaw in every twelfth request; the set is 60 requests, six flaw types each occur five times. export function set(n = 60) { const FLAW = { 6: { path: "/unknown" }, // unrecognized path 7: { trackingNo: undefined }, // schema: required field missing 8: { bodyBytes: 9000 }, // body size exceeded 9: { token: "broken" }, // malformed token 10: { trackingNo: "TR-0001" }, // unregistered shipment 11: { token: "b.M3.sig" }, // someone else's shipment }; return Array.from({ length: n }, (_, i) => ({ ...BASE, ...(FLAW[i % 12] ?? {}) })); } // Gatekeeper: looks only at the request itself (path, schema, size, token shape). export const GATE = { path: (r) => r.path === "/tracking", schema: (r) => typeof r.trackingNo === "string" && /^TR-\d{4}$/.test(r.trackingNo), size: (r) => r.bodyBytes <= 2048, token: (r) => /^b\.M\d+\.sig$/.test(r.token), }; // Application: two checks that look at domain data; cannot be given at the gate. export const APPLICATION = { exists: (r) => SHIPMENT[r.trackingNo] !== undefined, authorization: (r) => SHIPMENT[r.trackingNo]?.owner === r.token.split(".")[1], }; export const passes = (checks, r) => Object.values(checks).every((d) => d(r));
Token validation was built in the Authentication and Authorization course; the token check here
does not verify the signature, it only checks the shape — that is what can be given at the gate.
// gate/measure.mjs — measures the layout with and without a gatekeeper, then ties it to the K01 calculation import { set, GATE, APPLICATION, passes } from "./request.mjs"; const requests = set(); const invalid = requests.filter((r) => !(passes(GATE, r) && passes(APPLICATION, r))); const heldAtGate = invalid.filter((r) => !passes(GATE, r)); const bytes = (list) => list.reduce((t, r) => t + r.bodyBytes, 0); const reaching = (hasGate) => (hasGate ? requests.filter((r) => passes(GATE, r)) : requests); const FIELD = ["requests reaching the application", "invalid requests reaching the application", "body bytes reaching the application", "units touching untrusted input", "application's externally exposed address", "units carrying validation code"]; const measure = (hasGate) => { const u = reaching(hasGate); return { "requests reaching the application": `${u.length}/${requests.length}`, "invalid requests reaching the application": u.filter((r) => !passes(APPLICATION, r) || !passes(GATE, r)).length, "body bytes reaching the application": bytes(u), "units touching untrusted input": hasGate ? 1 : 3, "application's externally exposed address": hasGate ? 0 : 1, "units carrying validation code": hasGate ? 1 + 3 : 3, }; }; const measurement = [["no gate", measure(false)], ["gatekeeper", measure(true)]]; console.log(`${"measure".padEnd(42)}${measurement.map(([a]) => a.padStart(14)).join("")}`); for (const a of FIELD) console.log(`${a.padEnd(42)}${measurement.map(([, o]) => String(o[a]).padStart(14)).join("")}`); const ratio = heldAtGate.length / invalid.length; console.log(`\ninvalid requests = ${invalid.length}/${requests.length}, ` + `held at gate = ${heldAtGate.length}, not held at gate = ${invalid.length - heldAtGate.length}`); console.log(`ratio of invalid held at the gate = ${ratio.toFixed(3)} (${Object.keys(GATE).length} gate ` + `checks, ${Object.keys(APPLICATION).length} application checks)`); // K01 Back-of-the-Envelope Estimation: peak edge 513.89, peak read 416.67 requests/s (computed-value class) const EDGE_PEAK = 513.89, READ_PEAK = 416.67, BEHIND_CACHE = 41.67; console.log(`\n${"T3".padEnd(6)}${"requests/s behind gate".padStart(25)}${"reads/s".padStart(11)}` + `${"behind cache/s".padStart(17)}`); for (const T3 of [0.08, 0.16]) { // T3: the invalid share of edge requests const remaining = 1 - T3 * ratio; console.log(`${T3.toFixed(2).padEnd(6)}${(EDGE_PEAK * remaining).toFixed(2).padStart(25)}` + `${(READ_PEAK * remaining).toFixed(2).padStart(11)}${(BEHIND_CACHE * remaining).toFixed(2).padStart(17)}`); } console.log(`no gate (even for T3 = 0.08) -> ${EDGE_PEAK.toFixed(2)} requests/s, ` + `${READ_PEAK.toFixed(2)} reads/s, ${BEHIND_CACHE.toFixed(2)} behind cache/s`);
measure no gate gatekeeper requests reaching the application 60/60 40/60 invalid requests reaching the application 30 10 body bytes reaching the application 51600 4800 units touching untrusted input 3 1 application's externally exposed address 1 0 units carrying validation code 3 4 invalid requests = 30/60, held at gate = 20, not held at gate = 10 ratio of invalid held at the gate = 0.667 (4 gate checks, 2 application checks) T3 requests/s behind gate reads/s behind cache/s 0.08 486.48 394.45 39.45 0.16 459.08 372.23 37.23 no gate (even for T3 = 0.08) -> 513.89 requests/s, 416.67 reads/s, 41.67 behind cache/s
Reading the Numbers
Twenty of the thirty invalid requests are held at the gate, ten are not: the ratio is 0.667. The
ones not held are requests asking about an unregistered shipment and requests for someone else’s
shipment; neither can be told apart without reading the shipment record. The gatekeeper therefore
does not remove validation, it splits it in two, and units carrying validation code rises
from 3 to 4. The pattern’s name can be misleading: a gate does not stand in for the application’s
own validation.
The byte row gives the biggest difference: body bytes reaching the application fall from 51,600 to 4,800 bytes, 0.093 times. Nearly all of the drop comes from the five oversized requests. A request that never reaches the application is not just a saved operation, it is an unparsed body.
The last two rows carry the pattern’s real purpose. units touching untrusted input falls from 3
to 1, application's externally exposed address from 1 to 0. Code that parses the raw body,
checks its shape, and rejects what exceeds the limit gathers into a single unit; application units
see only a request that has already passed the gate and whose shape is known. This is the same
trust-boundary question from the third lesson, and it carries the same cost: if the gate can be
bypassed, the 0/3 protection assumption collapses.
Back to the Calculation
The model’s invalid ratio (30/60) is not realistic and is not presented as such. The real ratio is this course’s assumption (T3 = 0.08: eight percent of edge requests are formally invalid). The reasoning is that stale client versions, expired tokens, and browser retries produce a steady baseline; it is not added to K01’s table, because K01 only counted answered requests. The 0.667 coming from the model, by contrast, is not an assumption but a ratio that follows from the check lists.
Combined, the two move three of K01’s numbers at once. The peak edge rate, 513.89 requests/s,
passes to 486.48 requests/s behind the gate; peak reads fall from 416.67 to 394.45, reads behind cache/s from 41.67 to 39.45. Without a gate, none of the three change. When T3 doubles — sixteen
percent invalid — the numbers become 459.08, 372.23, and 37.23: the gain grows in direct
proportion to T3, because the share the gate holds stays fixed.
The size of the numbers is also worth reading: when two-thirds of the eight-percent-invalid requests are held, the edge rate falls to only 0.947 times. A gatekeeper is not a scaling tool; the real gain it produces sits in the table above — units touching untrusted input and the application’s externally exposed address.
Summary
- The gatekeeper isolates validation into a separate layer: only that layer sees untrusted input, the application receives a validated request.
- A check that can be given at the gate is one that looks at the request itself; in the sixty-request set, twenty of thirty invalid requests were held at the gate, ten were not (ratio 0.667).
- Validation is not removed, it is split: units carrying validation code rise from 3 to 4.
- Requests reaching the application fall from 60/60 to 40/60, the body from 51,600 to 4,800 bytes (0.093 times); nearly all of the drop comes from oversized requests.
- The pattern’s real gain is isolation: units touching untrusted input fall from 3 to 1, the application’s externally exposed address from 1 to 0.
- Back to K01 (with the T3 = 0.08 assumption): the 513.89 requests/s at the edge falls to 486.48 behind the gate, peak reads from 416.67 to 394.45, reads behind cache from 41.67 to 39.45; at T3 = 0.16, 459.08, 372.23, and 37.23 respectively.
Course Wrap-Up
The course designed the request’s path from the user to the application stop by stop, and at every stop it asked the same three questions: what decision does this stop make, which number in the introductory course’s calculation does it move, and what number grows in exchange. The measures the lessons left behind are gathered in the table below.
| Lesson | The stop’s decision | K01 number it moves | What grows in exchange |
|---|---|---|---|
| Domain Name System Design | routing policy | 1.171 distance units for latency-based (geolocation 1.710, weighted 2.144) | failover window 39–905 s, 0.50%–14.28% of the monthly outage budget |
| Content Delivery Networks | cache location | reads reaching the application 416.67 → 41.76 requests/s | 480-byte body takes 649 bytes on the wire (1.352); requests reaching the store stayed at 138.89 |
| Push and Pull Based Distribution | content propagation model | pull reaches the origin at 42.34 requests/s — K01’s 41.67 preserved | push peak 1,166.64 pushes/s, 4.48 Mbit/s and 6.91 GB held at the edges |
| Static Content Hosting | content path separation | application process 3,291.67 → 513.89 requests/s, 241.60 → 1.60 Mbit/s | 2,777.78 requests/s and 240.00 Mbit/s moved to the content process |
| Role of the Load Balancer | distribution + health check | 513.89 requests/s needs at least 3 replicas; dropped requests per failure 102,778 → 34.26 | the check’s own load 15 requests/s, 2.9% of the edge load |
| Layer 4 Balancing | connection-level distribution | decision 51.39 decisions/s (6 for 63 requests) | request skew 4.00; holding the target needs 5 replicas instead of 3 |
| Layer 7 Balancing | content-aware routing | request skew 4.00 → 1.07, streams in separate pools | decision 51.39 → 513.89 decisions/s; 4 replicas instead of 3 |
| Balancing Algorithms | distribution algorithm | displaced key 74.85% → 26.48% | warmup store-reaching 138.89 → 238.18 requests/s (419.57 with modulo hashing) |
| Load Balancer and Reverse Proxy | role separation | when the replica-list process dropped, content path answered 6/6 | hop 2 → 3; single-copy-hop monthly outage 215.6 → 258.6 min |
| Session Stickiness | state bound to a replica | replicas needed for the 0.50 utilization target 3 → 4 | imbalance 1.000 → 1.530 (sticky-modulo), 1.411 (sticky-consistent); when a replica drops, sticky-consistent loses 30/100 sessions and 21.78% of requests |
| API Gateway | edge responsibility placement | repeated lines 135 → 45, units per responsibility 3 → 1 | 513.89 requests/s at the edge collects into one component; hop-requests/s 513.89 → 1027.78; flows answered when the gateway stops 0/3 |
| Gateway Routing and Aggregation | multiple calls in one request | edge requests 833.34 → 416.67 requests/s (×0.50) | body 402 → 741 bytes; egress 1.60 → 2.47 Mbit/s |
| Gateway Offloading | check moved to the edge | lines 171 → 107; 4/6 checks at the edge, running at 513.89 requests/s | unprotected service 0/3 → 3/3, or validation 513.89 → 1027.78 times/s |
| Backends for Frontends | client-specific body | unused fields 37 → 0; 583.34 → 416.67 requests/s, 3.46 → 1.93 Mbit/s | edge units 1 → 3; batch endpoints the service must offer 0 → 2 |
| Ambassador and Sidecar Patterns | common work handed to a separate process | contracts the service sees 1 (external 2); units that can use it 4/6 → 6/6 | total processes 6 → 12; hop-requests/s 1027.78 → 1541.67 |
| Gatekeeper Pattern | validation isolated | behind the gate 513.89 → 486.48 requests/s; reads 416.67 → 394.45 | units carrying validation code 3 → 4; invalid not held at the gate 10/30 |
The table’s rule deserves to be named: every stop was tied back to a calculation that comes out of the introductory course’s assumption table. A decision made at the edge is measured by one of three numbers — requests reaching the application, bytes crossing the boundary, and the answered-request ratio when a unit drops. An edge decision that ties to none of these three is not a defensible decision; the right column never being empty in any row is the other half of that rule. The three assumptions the course added (the tracking page also asking for the fee, the client mix, the invalid-request ratio) were not mixed into K01’s table; they were written down under their own names, reasoning, and sensitivities.
The question the course leaves behind sits one layer forward. The request has now reached the application: its name was resolved, it passed through the edge cache, it landed on a replica, it passed through the gateway and the gate. But the application’s own internal service interaction was not designed — whether the services are stateless, how they find each other’s addresses, how long they wait and how many times they retry when a call does not answer, and whether a workflow spread across several services is built with choreography or orchestration. This course’s gateway, while calling two services in parallel and merging their responses, silently assumed an answer to every one of these questions. The next course, The Application Layer and Service Interaction, opens those assumptions.
To keep your progress and take notes, Log in
My notes
Log in to take notes.