R13 · Week 13 Recall & Drill
Week 13 revision: TCP as a byte stream, L4 versus L7 load balancing, cache-aside and stampedes, CDN cache keys and Vary, and Docker layers as processes not VMs.
Original publication date unrecordedUpdated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- Narrate a request end to end — name resolution, handshake, transport, response, teardown — and point at the layer any given failure lives in.
- Explain why a byte-stream transport forces you to frame your own messages, and write a length-prefixed protocol.
- Choose transport-level versus application-level load balancing in one sentence, and say why equal request counts do not mean equal load.
- Implement cache-aside with expiry, negative caching, and stampede protection, and name the four canonical cache failure modes.
- Set caching and variance headers correctly, and explain why cache key composition determines hit ratio.
- Order a Dockerfile for layer-cache efficiency and explain why a container is a process rather than a virtual machine.
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 19 sections
Assessment scope
Attempt the recall prompts unaided, then work through the implementations and answer keys. The local programs use explicit fixtures and independent checks; product behavior is tied to named primary sources, not inferred from the toy models. Service deployment and production readiness are separate from this conceptual review. Timing budgets are adjustable practice suggestions.
Sessions covered
- S061 — Networking I — TCP/IP, DNS, Sockets
- S062 — Networking II — Load Balancers L4 vs L7, Reverse Proxies
- S063 — Caching — Cache-Aside, Write-Through, TTLs, Invalidation
- S064 — CDN — Edge, Cache Hierarchies, Cache-Control
- S065 — Docker — Images, Layers, Dockerfile, Networking
By the end of this revision you can
- Narrate a request end to end — name resolution, handshake, transport, response, teardown — and point at the layer any given failure lives in.
- Explain why a byte-stream transport forces you to frame your own messages, and write a length-prefixed protocol.
- Choose transport-level versus application-level load balancing in one sentence, and say why equal request counts do not mean equal load.
- Implement cache-aside with expiry, negative caching, and stampede protection, and name the four canonical cache failure modes.
- Set caching and variance headers correctly, and explain why cache key composition determines hit ratio.
- Order a Dockerfile for layer-cache efficiency and explain why a container is a process rather than a virtual machine.
90-min structure
| Block | Minutes | Practice |
|---|---|---|
| Warm-up recall | 5 | Summarize the five subjects without notes. |
| Blank-page reconstruction | 30 | Answer the fifteen prompts, then check the subject solutions. |
| Hands-on drill | 30 | Trace and run the bounded implementations; explain their limits. |
| Quiz + misconception | 15 | Defend the corrected answers with a failure case. |
| Gap analysis + preview | 10 | Record a discriminating retest and connect the next subjects. |
The 90-minute budget is pacing guidance, not a claim that full execution and every extension fit every learner's session.
Blank-page reconstruction · 30 min
S061 · Networking I
- Narrate the three-packet handshake, saying what each packet establishes.
- Explain why the transport is a byte stream, and the bug that creates when you write your own protocol.
- Say what a record's time-to-live controls, and why lowering it cannot invalidate answers already cached.
Answer key — follow the layers. A browser first checks appropriate local/resolver DNS caches; on a miss, recursive resolution obtains the address, possibly following aliases. For a new TCP connection the client sends SYN with its initial sequence number; the server replies SYN+ACK acknowledging the client's number and supplying its own; the client's ACK completes the ordinary handshake. HTTPS then negotiates TLS and validates the server identity before sending HTTP application bytes. HTTP/2 can multiplex requests on that connection; HTTP/3 uses QUIC rather than this TCP path. DNS failure precedes connection setup, connection refusal differs from timeout, and a valid HTTP 500 means an application response was obtained rather than a DNS failure.
TCP preserves byte order, not application write boundaries: hello plus world can be read as helloworld, or as several smaller chunks. A positive send result is a count accepted by the local stack, possibly smaller than requested; even sendall returning does not prove the peer durably processed the operation. An application acknowledgement needs an explicit meaning such as “committed order ID 17,” plus idempotency for a lost acknowledgement.
DNS TTL limits ordinary cache reuse of that answer. Lowering the authoritative TTL changes future answers, not the remaining TTL of copies already cached; it can help later refreshes, not retroactively invalidate every client. Serve-stale policies and application caching further complicate a deadline. TCP close normally exchanges FIN/ACK independently in each direction; TIME-WAIT protects against delayed duplicates and helps complete close reliably, and is not the same as an application leaking CLOSE-WAIT sockets. Connection reuse can reduce churn, but inspect states, traffic and limits before tuning timers.
S062 · Networking II
- Give a one-sentence rule for choosing transport-level versus application-level balancing.
- Explain why fewest-connections often beats round-robin.
- Describe connection draining in one sentence.
Answer key — choose the information needed for routing. L4 balancing routes transport flows without needing an HTTP path; L7 reverse proxying can route by host/path and apply HTTP policy, often terminating TLS. Choose L4 for opaque protocols or end-to-end encrypted passthrough; choose L7 when request semantics are required. A TCP flow and an HTTP/2 request are not equivalent scheduling units.
Round-robin equalizes assignment counts, not CPU, duration or downstream work. Fewest active connections is a useful proxy for long-lived unequal connections, not a guarantee of least remaining work: one connection can carry many expensive streams. Weighted routing accounts for capacity; queue depth, service time and saturation provide additional signals. Draining stops new assignments while allowing existing work a bounded time to finish, after which the documented termination policy applies.
A listening socket proves little about a request's dependency path. Readiness determines whether to route new work; liveness should detect a state that restarting can repair. Making every liveness probe depend on a shared database can restart all healthy processes during a database incident. A readiness dependency check also needs a deliberate policy: removing every backend may be worse than serving a bounded degraded response.
S063 · Caching
- Explain cache-aside in one sentence, and give one workload where writing through is a better fit.
- Define a stampede and give one line of defence.
- Say why caching the absence of a result matters.
Answer key — cache-aside is a consistency decision. Read the cache, fetch from the origin on a miss, then populate with an expiry; writes update the origin and invalidate or refresh affected entries under a declared race policy. Write-through can suit a read-heavy catalog with centrally controlled writes, but a cache+database dual write is not automatically atomic. A TTL bounds residency since fill, not necessarily age since the latest origin write: an old in-flight read can repopulate after invalidation. Versions or coordinated invalidation are needed for a stronger guarantee.
The four failure families here are stale values/invalidation races, capacity eviction and churn, cache outage/cold-start overload, and stampedes (one hot key or synchronized many-key expiry). Negative caching stores a confirmed “not found” with a short TTL; distinguish it from an origin timeout/error and invalidate when an object is created. Single-flight coalesces concurrent fills for one key; jitter spreads many keys' expiries. Neither substitutes for bounded origin concurrency, a cache capacity policy, authorization-aware keys and a documented cache-outage fallback. The Step 3 implementation has a deliberately bounded key universe so its lock and entry dictionaries cannot grow without limit.
S064 · CDN & Edge
- Distinguish the client-facing maximum age from the shared-cache maximum age.
- Explain what declaring variance on encoding does, and what breaks without it.
- Say why an intermediate shield tier exists between edge and origin.
Answer key — derive the reuse key. In Cache-Control: public, max-age=60, s-maxage=300, browsers use a 60-second freshness lifetime while a shared cache uses the 300-second shared-cache override, subject to age and other cache rules. Vary: Accept-Encoding requires compatible request-header values when choosing a stored variant. Do not confuse Vary with access control: authenticated/personalized representations need their own private/no-store policy or carefully scoped shared-cache contract.
The primary cache key includes at least method and target URI; CDNs may expose additional configurable key rules. Only normalize an irrelevant query parameter after proving it does not change representation or authorization. Dropping a meaningful locale or tenant discriminator improves apparent hit ratio by serving wrong data. Investigate eligibility/freshness, key fragmentation and working-set/request distribution together; low hit ratio is not proof of one cause. An origin shield is an intermediate shared tier that can coalesce misses from many edges, at the cost of an extra hop and a new capacity/failure boundary. no-cache permits storage with validation; no-store prohibits storage under the directive, not all possible prior copies.
S065 · Docker
- State the difference between an image and a container in one sentence.
- Explain why instruction ordering in a build file matters.
- Explain when multi-stage builds make the final image smaller and what must be left behind.
Answer key — image bytes versus running processes. An image is an immutable package of filesystem layers and runtime configuration; a running Linux container is an isolated process or process group instantiated from it, with a writable container layer. Ordinary Linux containers share the kernel of their Linux host (which may itself be a VM), rather than booting a separate guest kernel per container. Namespaces and resource controls define isolation; sharing a kernel is not the same security boundary as an independent VM. Multiple processes can run inside a container, so “one container equals exactly one process” is not a required invariant.
Each layer records filesystem changes. Deleting a file in a later layer hides it in the merged view but does not erase bytes from the retained earlier layer. Copying credentials and then deleting them is therefore not safe secret handling. Keep secrets out of the build context and use supported secret/SSH mounts for authorized build access; commands can still leak mounted values if they deliberately copy or log them.
A multistage build starts a build stage with compilers/development tools, then starts a separate runtime stage and selectively copies the runtime artifact, for example COPY --from=build /out/app /app/app. Only the runtime stage's base/layers and copied bytes belong to the final image; the compiler can remain in the build-stage cache. The final stage must still supply required dynamic libraries/runtime files. This can reduce the final image, not guarantee a smaller total builder cache or a secure artifact. Starting another stage from the whole build stage, or copying its entire filesystem, can retain the very material you meant to exclude. The two Step 4 Dockerfiles teach dependency-cache ordering; they are not themselves multistage examples.
On an ordinary bridge network, a container has its own network interface and loopback: connecting to 127.0.0.1 inside it reaches that container, not automatically the host or a sibling. Service discovery/network attachment and explicit host port publishing are separate choices. None of these source-reviewed mechanisms claims a Docker build or live networking test.
Hands-on drill · 30 min
Run these Python fences in order as one Python 3 program. All I/O is in memory; the controlled chunks expose a byte-stream bug without hoping a live socket happens to coalesce writes. Dockerfiles are reviewed examples, not builds.
Step 1 — the byte-stream framing bug (9 min)
A four-byte unsigned network-order length precedes each payload. Bound the declared length before allocating or waiting; distinguish clean EOF at a frame boundary from a truncated header/body. In a real connection also enforce read deadlines and total connection/output budgets.
import struct
class Decoder:
def __init__(self, max_frame=1024):
self.buf = bytearray()
self.max_frame = max_frame
def feed(self, chunk):
self.buf.extend(chunk)
out = []
while len(self.buf) >= 4:
size = struct.unpack('!I', self.buf[:4])[0]
if size > self.max_frame:
raise ValueError('frame exceeds contract')
if len(self.buf) < 4 + size:
break
out.append(bytes(self.buf[4:4+size]))
del self.buf[:4+size]
return out
def finish(self):
if self.buf:
raise EOFError('truncated frame')
def encode_messages(messages):
return b''.join(struct.pack('!I', len(m)) + m for m in messages)
messages = [b'hello', b'a much longer second message', b'', b'third']
wire = encode_messages(messages)
for split in range(len(wire) + 1):
decoder = Decoder()
got = decoder.feed(wire[:split]) + decoder.feed(wire[split:])
decoder.finish()
assert got == messages
naive_reads = [b''.join(messages)]
assert naive_reads != messages
print('all two-chunk splits recover', len(messages), 'frames')Trace: three header bytes leave the decoder waiting; the fourth reveals length 5; the next five bytes emit hello. Leftovers become the next header, not part of the first message. A zero-length frame consumes its four-byte header and emits b''; it must not be mistaken for socket EOF. Exercise: feed one byte at a time, then remove the final byte; answer: the first run is identical, the second raises EOFError at finish. An excessive length raises before a body arrives. This parser returns lists for bounded chunks; callers must also bound chunk sizes and how long returned messages are retained.
Step 2 — round-robin does not equalise work (6 min)
costs = [1, 1, 20, 1, 1, 20]
def assign(costs, workers=3, policy='round_robin'):
if workers <= 0 or any(c < 0 for c in costs):
raise ValueError('positive workers and nonnegative costs required')
if policy not in ('round_robin', 'least_work'):
raise ValueError('unknown policy')
work, counts = [0]*workers, [0]*workers
for i, cost in enumerate(costs):
target = i % workers if policy == 'round_robin' else min(range(workers), key=lambda j: work[j])
work[target] += cost
counts[target] += 1
return counts, work
assert assign(costs) == ([2, 2, 2], [2, 2, 40])
assert assign(costs, policy='least_work') == ([3, 2, 1], [22, 2, 20])
print('round robin', assign(costs), 'least assigned work', assign(costs, policy='least_work'))The imbalance follows exactly from indices 2 and 5 sharing backend 2. The greedy policy selects a current minimum before adding the next job's cost; it does not foresee that the last cost is 20 and is not optimal scheduling. This is cumulative assigned work with no elapsed-time/completion model, not a simulation of least-connections or a latency benchmark. Try equal costs: round-robin is already balanced. In a live system unknown costs, connection multiplexing and heterogeneous backends can overturn this proxy's ranking.
Step 3 — stampede and synchronised expiry (8 min)
This bounded single-process cache stores None as a negative result, never as a miss sentinel. It checks freshness while holding a per-key lock and does not cache exceptions. The origin must have its own timeout; a waiting caller otherwise inherits the fill delay.
import threading
import random
from collections import Counter
class Cache:
def __init__(self, keys, origin, clock, ttl=10, negative_ttl=2):
if ttl <= 0 or negative_ttl <= 0:
raise ValueError('positive TTLs required')
self.locks = {k: threading.Lock() for k in keys}
self.entries = {}
self.origin, self.clock = origin, clock
self.ttl, self.negative_ttl = ttl, negative_ttl
def get(self, key):
with self.locks[key]: # unknown keys rejected: bounded teaching universe
hit = self.entries.get(key)
if hit is not None and self.clock() < hit[0]:
return hit[1]
value = self.origin(key)
ttl = self.negative_ttl if value is None else self.ttl
self.entries[key] = (self.clock()+ttl, value)
return value
def invalidate(self, key):
with self.locks[key]:
self.entries.pop(key, None)
now = [0]
origin_calls = Counter()
def origin(key):
origin_calls[key] += 1
return None if key == 'missing' else 'value'
cache = Cache(['hot', 'missing'], origin, lambda: now[0])
assert cache.get('missing') is None and cache.get('missing') is None
assert origin_calls['missing'] == 1
now[0] = 2
cache.get('missing')
assert origin_calls['missing'] == 2
barrier = threading.Barrier(8)
def reader():
barrier.wait(timeout=5)
return cache.get('hot')
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=8) as executor:
assert list(executor.map(lambda _: reader(), range(8))) == ['value']*8
assert origin_calls['hot'] == 1
rng = random.Random(7)
fixed = Counter([100]*1000)
jittered = Counter(100 + rng.randrange(-20,21) for _ in range(1000))
assert max(fixed.values()) == 1000 and max(jittered.values()) < 1000
print('origin calls', dict(origin_calls), 'peak expiry bin', max(jittered.values()))Unprotected schedule oracle: eight reads all observe absence before any of their eight fills publishes; hence eight origin calls are possible. The protected execution above verifies one fill for this eight-reader run, not all distributed schedules. Invalidation under the same key lock prevents an earlier local fill from publishing after that invalidation, but it does not make origin writes atomic with the cache or coordinate other processes. Jitter lowers this seed's peak one-second expiry count; it does not prevent eight callers missing the same single key. For an origin exception no new entry is published: a cold key remains absent, while an already expired entry remains expired and is not served by this code. Bound retries and pressure rather than treating errors as “not found.” The fixed key universe bounds entry/lock counts, not arbitrary value bytes or waiting threads; this fixture uses small immutable strings. A production cache needs value-size/capacity and admission/concurrency limits too.
Step 4 — layer cache ordering (7 min)
# Poor source-change reuse; illustrative, not built
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "app.py"]# Dependency installation can be reused for a source-only change
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN useradd --create-home appuser
USER appuser
CMD ["python", "app.py"]Worked cache decision: hold the base digest, build arguments and requirements.txt fixed, change only app.py. In the first file COPY . . changes, so its dependent install step must rerun. In the second, the manifest copy and install can reuse cache; invalidation begins at COPY app.py .. If requirements change, both must reinstall; if the mutable base tag resolves to a new digest, earlier assumptions also change. A reused RUN layer does not spontaneously check for new package releases. Pin/lock dependencies and choose an explicit refresh policy for reproducibility and patching. --no-cache-dir affects pip's download cache, not Docker's instruction cache. These snippets teach reuse and a nonroot runtime, not a complete production image contract.
Recall quiz — explain before checking
- Does successful send imply committed work? No: local acceptance is not peer processing; only a defined application result can acknowledge the business operation, and an ambiguous lost response still needs reconciliation/idempotency.
- Can one recv be one message? Only by coincidence; the parser must retain partial headers/bodies and iterate over multiple frames.
- Why do equal counts overload one backend? Two costly jobs can share a backend; connection counts are also imperfect proxies.
- What three CDN checks come first? Eligibility/freshness directives, the actual configured key, and Vary-driven variants; verify correctness before removing discriminators.
- What must a cache design own? Invalidation/staleness, capacity/eviction, outage fallback and cold/hot-key origin pressure, including negative caching, bounded fills, jitter and scoped single-flight. A hit is not proof of freshness.
Answer constraints and corrections
TCP is a byte stream, not a message API; application framing and acknowledgements carry distinct semantics. A successful send is not proof of durable application processing. Load policy cannot guarantee perfect balance with unknown costs. Cache-control and Vary define response reuse; Docker layer reuse depends on actual build inputs.
Gap analysis + next week preview · 10 min
- Can your parser handle all tested splits, empty messages, excessive lengths and truncated EOF without relying on socket scheduling?
- Does the count/work trace make the proxy limitation of least-connections clear?
- Can you distinguish one-key coalescing, many-key jitter, negative caching and invalidation/outage behavior?
Mark each answer unaided, hinted or not yet; select a failed assumption and use a new counterexample for the retest rather than memorizing one fixture. Keep the input contract, boundary case and independent expected answer next to any execution result.
Next week: S066–S070 cover Kubernetes basics/advanced configuration, Azure identity/networking, infrastructure as code and CAP/PACELC. Distinguish a process/image from the controllers and traffic policies around it.
Corrective source notes
-
What is a container? | Docker Docs — ordinary containers are isolated processes sharing their host kernel, not a separate guest OS per container. Retrieved and passage reviewed 2026-10-09.
-
What is an image? | Docker Docs and Understanding image layers — immutable layers supply image files; a container's writable layer leaves the original layers untouched. Retrieved and passages reviewed 2026-10-09.
-
RFC 9293 TCP, August 2022 — TCP supplies an ordered byte stream, not application message frames. TCP segment/read/write boundaries do not preserve application messages; length framing must buffer partial and multiple messages. TIME-WAIT allows termination acknowledgement and delayed-segment protection; it is not identical to an application descriptor leak. Retrieved 2026-10-07; exact passage reviewed 2026-10-07.
-
RFC 1035 DNS, November 1987 — DNS TTL governs caching of a received answer; reducing future TTL cannot invalidate an already cached answer retroactively. Retrieved 2026-10-07; exact passage reviewed 2026-10-07.
-
NGINX HTTP load balancing — Round-robin and least-connected routing use different proxies; least-connected counts active connections rather than knowing future work. Retrieved 2026-10-07; exact passage reviewed 2026-10-07.
-
RFC 9111 HTTP caching, June 2022 — Shared caches use s-maxage before max-age when calculating explicit freshness. Vary affects which stored representation can be reused without revalidation; removing discriminators without semantic proof is unsafe. Retrieved 2026-10-07; exact passage reviewed 2026-10-07.
-
Build cache invalidation | Docker Docs — A changed build input invalidates dependent later work; stable dependency inputs should precede frequently changing source. Retrieved 2026-10-07T20:10:41.626488+00:00; exact passage reviewed 2026-10-07.
-
Multi-stage builds | Docker Docs — Multi-stage builds allow selectively copying runtime artifacts while leaving build-only material behind. Retrieved 2026-10-07T20:10:41.654047+00:00; exact passage reviewed 2026-10-07.
-
Build secrets | Docker Docs — Build credentials should use supported secret/SSH mounts rather than persistent build arguments/environment layers. Retrieved 2026-10-07T20:10:42.135087+00:00; exact passage reviewed 2026-10-07.
-
Bridge network driver | Docker Docs — Containers on a bridge have network interfaces in that network; container and host networking/publishing are distinct. Retrieved 2026-10-07T20:10:39.917857+00:00; exact passage reviewed 2026-10-07.
These passages support the named propositions, not blanket product certification. The lesson-specific derivations and fixtures are separate evidence; runtime boundaries are stated beside each worked exercise.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.