Containers Course — 07 · Ingress, Observability, RBAC and GitOps
The workloads run, they are scheduled sensibly, and they survive a rollout. What remains is everything around them: how external traffic reaches the cluster, how Pods are prevented from talking to each other, how you find out what is wrong at three in the morning, who is allowed to do what, and how manifests get from a Git repository into a cluster without a human running from a laptop. This is the last post of the base course, and it is the one that turns a working cluster into an operable platform.
> Series map: 00 Containers from First Principles → 01 Images and Dockerfiles → 02 Networking, Volumes and Compose → 03 Registries, Supply Chain and Security → 04 Kubernetes Architecture and Core Objects → 05 Config, Storage and Stateful Workloads → 06 Scheduling, Resources, Probes and Rollouts → 07 Ingress, Observability, RBAC and GitOps (this post — final base post).
Learning Outcome
- Route external HTTP traffic, terminate TLS, and automate certificates with cert-manager.- State the current status of the Gateway API and decide whether to start there instead of Ingress.- Write a default-deny NetworkPolicy, and judge honestly whether a service mesh is worth its cost.- Assemble the metrics/logs/traces triad on Kubernetes and alert on symptoms rather than causes.- Debug a broken Pod with describe, previous logs, events and ephemeral debug containers.- Design least-privilege RBAC, apply Pod Security Admission, and enforce policy with an admission controller.- Choose between Helm and Kustomize, run delivery with Argo CD or Flux, and control managed-cluster cost.
Chapter 1 — Getting traffic in
Terminology
- Ingress — a built-in API object describing HTTP/HTTPS routing rules (host, path, backend Service, TLS).- Ingress controller — the component that actually implements those rules. Kubernetes ships the API, not an implementation.- Gateway API — the newer, role-oriented, extensible routing API that supersedes Ingress. Installed as CRDs, not built in.- TLS termination — decrypting HTTPS at the edge so backends receive plain HTTP.- ACME — the protocol (used by Let's Encrypt) for automated certificate issuance.
Ingress
A Service per application means one cloud load balancer per application, each with its own IP, its own bill and its own certificate. Ingress collapses that into one entry point that routes by hostname and path.
The controller watches Ingress objects and reconfigures its own proxy. Note what the API does not cover: rate limiting, header rewriting, request mirroring, timeouts, retries, authentication. Every controller exposes those through vendor-specific annotations, which is precisely why Ingress manifests are not portable between controllers and why the Gateway API exists.
The ingress controller landscape, as of 2026
This changed materially and you need the current picture. The long-time default, ingress-nginx, was retired in March 2026: maintenance ceased, the repository was archived, and the planned successor project (InGate) was abandoned before reaching maturity. The Kubernetes SIG Network and Security Response Committee guidance is to migrate to the Gateway API or to another actively maintained controller.
| Option | Notes ||--------|-------|| Gateway API with a conformant implementation (Envoy Gateway, Traefik, Istio, Cilium, kgateway, cloud-native gateways) | the recommended direction for new clusters || Traefik | actively maintained, supports both Ingress and Gateway API, good developer ergonomics || HAProxy / F5 NGINX (commercial) / Envoy-based controllers | maintained Ingress implementations if you must stay on the Ingress API || Cloud-native — AWS Load Balancer Controller, GKE Ingress, AGIC | provisions the cloud's own L7 load balancer; least to operate, most vendor-coupled || ingress-nginx | retired — running it means running unmaintained, unpatched edge software |
If you are on ingress-nginx today, treat migration as a scheduled project, not an emergency: inventory your annotations first (that is where the work is), then use the conversion tool as a starting point and review everything it emits.
TLS with cert-manager
cert-manager is a controller that turns a certificate request into a Kubernetes Secret and keeps it renewed.
With the annotation on an Ingress, cert-manager notices the block, solves the ACME challenge, writes , and renews it automatically before expiry.
> Common pitfall: developing against and burning through Let's Encrypt's rate limits in an afternoon. Use the ACME staging endpoint until the whole chain works, then switch. The staging certificate will not validate in a browser — that is the point.
The Gateway API
Gateway API replaces one overloaded object with a set of role-separated ones:
| Resource | Owned by | Purpose ||----------|----------|---------|| GatewayClass | infrastructure provider | which implementation, cluster-scoped || Gateway | platform / cluster operator | a listener: ports, protocols, TLS, which namespaces may attach || HTTPRoute / GRPCRoute / TLSRoute / TCPRoute | application team | the routing rules themselves |
Current status, stated plainly. Gateway API is a set of CRDs versioned independently of Kubernetes and installed separately. Its Standard (GA) channel has carried GatewayClass, Gateway and HTTPRoute since v1.0 in late 2023, with GRPCRoute following, and v1.5 (early 2026) promoted a further six features from Experimental to Standard. Several conformant implementations exist. The Ingress API remains GA and supported but is feature-frozen — all active development happens in Gateway API. Meanwhile the most widely deployed Ingress controller has been retired.
The practical reading: new clusters should start on the Gateway API; existing Ingress deployments keep working and should be migrated deliberately, prioritising any cluster still running ingress-nginx. Check the conformance report for your chosen implementation rather than assuming a feature is available — extended features are genuinely optional.
Chapter 2 — East-west traffic: CNI, NetworkPolicy and meshes
The CNI model
Kubernetes does not implement Pod networking. It defines a contract — CNI (Container Network Interface) — and requires that every Pod gets its own IP, that any Pod can reach any other Pod without NAT, and that nodes can reach all Pods. A plugin (Calico, Cilium, Flannel, AWS VPC CNI, Azure CNI, Weave) fulfils it, with very different implementations: overlay tunnels (VXLAN, Geneve), native routing (BGP), or cloud-native IP allocation from the VPC.
Two consequences: the default posture is completely flat — every Pod in every namespace can reach every other Pod, on every port — and NetworkPolicy is enforced by the CNI plugin, not by Kubernetes. A NetworkPolicy applied on a cluster whose plugin does not implement it is silently accepted and does nothing at all. Verify enforcement before you rely on it.
Default-deny, then open what is needed
Rules to internalise:
- Policies are additive whitelists. A Pod selected by any policy denies everything not explicitly allowed by some policy that selects it. A Pod selected by no policy allows everything.- with both and no rules is the idiomatic default-deny.- Always allow DNS egress explicitly. Forgetting it is the single most common NetworkPolicy incident: name resolution fails, every outbound call breaks, and the errors point everywhere except the network policy.- Policies are namespace-scoped and select by label. Cross-namespace rules need ; the automatic label makes that convenient.- Standard NetworkPolicy is L3/L4 only — IPs, ports, protocols. It cannot express "GET but not DELETE" or "only with a valid identity". CNI-specific CRDs (Cilium's , Calico's ) add L7 rules, and the emerging API adds cluster-wide rules that individual namespaces cannot override.
> Trick: roll out default-deny in audit mode first if your CNI supports it, or apply the deny policy to one low-risk namespace and watch the errors. Applying default-deny to a busy production namespace on a Friday is a well-documented way to spend a weekend.
Service mesh, honestly
A service mesh (Istio, Linkerd, Cilium's mesh mode) puts a proxy in the path of every Pod-to-Pod call — historically a sidecar, increasingly a per-node or ambient component. In exchange for that you get automatic mTLS between all workloads, L7 traffic control (retries, timeouts, circuit breaking, canary splitting by header or weight), uniform golden-signal telemetry for every service without touching application code, and identity-based authorisation policy rather than IP-based.
The costs are real and routinely understated: an extra network hop and a few milliseconds on every call; noticeable CPU and memory per Pod for sidecar deployments; a substantial new control plane to upgrade in lockstep with the cluster; failure modes that are genuinely hard to debug because the proxy is now between you and every symptom; and a body of concepts (VirtualService, DestinationRule, PeerAuthentication, or their equivalents) the whole team must learn. Most clusters do not need one. If you have a handful of services, NetworkPolicy plus TLS at the ingress plus a decent HTTP client library covers the same ground for a fraction of the operational cost. Reach for a mesh when you have a real, specific driver — a compliance requirement for encryption in transit everywhere, dozens of teams needing uniform traffic policy, or progressive delivery you cannot express any other way — and when you have someone who will own it. Linkerd is the smaller, simpler option; Istio ambient mode and Cilium remove the per-Pod sidecar cost; a mesh chosen because it was on a conference slide is a liability.
Chapter 3 — Observability
The triad
| Signal | Answers | Typical stack ||--------|---------|---------------|| Metrics | "is something wrong, and how wrong?" — cheap, aggregatable, alertable | Prometheus, Thanos/Mimir/Cortex, cloud-native metrics || Logs | "what exactly happened in this request?" — high detail, high volume | Fluent Bit or Vector → Loki, OpenSearch, or a managed backend || Traces | "where did the latency go across N services?" | OpenTelemetry → Jaeger, Tempo, or a vendor |
Metrics tell you that; traces tell you where; logs tell you why. You need all three, in that order of priority for a small team.
Prometheus and kube-state-metrics
The chart is the conventional starting point. It brings four things that are easy to confuse:
- Prometheus — scrapes and stores time series; discovers targets from the Kubernetes API.- node-exporter (DaemonSet) — node metrics: CPU, memory, disk, filesystem, network.- cAdvisor (inside the kubelet) — container resource metrics: , .- kube-state-metrics — object state metrics generated from the API server: , , . Not resource usage — the declared and observed state of objects.
That last distinction matters constantly: "is the container using too much memory" is cAdvisor; "have the desired replicas been available for ten minutes" is kube-state-metrics. Note also that metrics-server is a separate, minimal component that exists only to serve and the HPA — it is not a monitoring system and does not store history.
Alert on symptoms, not causes
The discipline that separates a usable on-call rotation from an ignored one: page on what users experience; make everything else a dashboard or a ticket.
Cause-based alerts ("CPU above 80 %", "a Pod restarted", "disk 70 % full", "replica count below desired") fire constantly, are usually harmless, and train people to ignore the pager. Symptom-based alerts describe a violated expectation:
- Error rate on exceeded 1 % of requests over 5 minutes.- p99 latency for checkout exceeded 2 s over 10 minutes.- The nightly export CronJob has not produced a successful Job in 26 hours.- The service is consuming its error budget fast enough to exhaust the month's allowance in under a day (multi-window, multi-burn-rate SLO alerting).
Useful frameworks: RED (Rate, Errors, Duration) for request-driven services, USE (Utilisation, Saturation, Errors) for resources, and the four golden signals (latency, traffic, errors, saturation). A small number of genuinely paging alerts beats a hundred noisy ones.
A short list of Kubernetes-specific conditions genuinely worth paging on: a Deployment with zero available replicas; persisting beyond a few minutes; Pods for more than N minutes (capacity); a node beyond the eviction timeout; PersistentVolume nearly full; certificate expiring within a week; API server error rate or latency elevated.
Logging when Pods are ephemeral
reads from the node's log files for a currently existing Pod. Delete the Pod — as every rollout, eviction and node scale-down does — and the logs are gone. reaches exactly one container generation back, and rotation discards the rest. That is not a logging strategy.
The standard architecture is a node-level agent (Fluent Bit or Vector as a DaemonSet) that tails , enriches each line with Kubernetes metadata (namespace, Pod, container, labels) from the API, and ships it to a store — Loki, OpenSearch, or a managed service. Requirements that pay for themselves:
- Log to stdout/stderr as structured JSON. Writing to a file inside the container means the agent never sees it. Unstructured text means you cannot query by field.- Carry the trace ID in every line. It is what joins the three signals.- Set retention by value, not by default. Logs are usually the largest line in an observability bill; 7 days hot plus 90 days in cheap object storage is a common shape.- Do not log secrets or personal data. Once shipped, it is in someone else's index and in their backups.
OpenTelemetry
OpenTelemetry is the vendor-neutral standard for producing and shipping all three signals: one set of APIs and SDKs, one wire protocol (OTLP), and the OpenTelemetry Collector as a pipeline of receivers, processors and exporters. On Kubernetes it is typically deployed twice — as a DaemonSet for node-local collection and enrichment, and as a Deployment (gateway) for aggregation, sampling and fan-out to backends. The OpenTelemetry Operator adds auto-instrumentation by injecting language agents into Pods via an annotation, which gets you traces from a Java, Python, Node or .NET service without editing its code.
The reason to standardise on it is portability: instrument once, and changing backends becomes a Collector configuration change instead of a re-instrumentation project. For a full worked treatment of OpenTelemetry — spans, context propagation, sampling strategy, collector configuration and correlating traces with database telemetry — see .
Debugging a broken Pod
An ordered toolkit, roughly cheapest first:
When the image is distroless or scratch and has no shell — as a hardened image should be (Post 03) — use an ephemeral debug container, GA since Kubernetes 1.25:
> Common pitfall: reaching for and finding no shell, then rebuilding the image with a shell "for debugging" and shipping that to production. Ephemeral containers exist precisely so that production images can stay minimal. Ensure your RBAC permits for the people who need it — and only them.
Chapter 4 — Security: RBAC, Pod Security and admission control
RBAC
Four object types, one simple model.
- Role — a set of permissions, namespaced.- ClusterRole — the same, but cluster-scoped; also the only way to grant access to cluster-scoped resources (nodes, PVs, CRDs) or to non-resource URLs.- RoleBinding — grants a Role or a ClusterRole to subjects within one namespace.- ClusterRoleBinding — grants a ClusterRole across the whole cluster.
Binding a ClusterRole with a RoleBinding is the most useful idiom: define the permission set once, grant it namespace by namespace.
Verbs: , , , , , , , , plus , and . RBAC is purely additive — there is no deny rule. If you need denial, that is admission control's job.
Three permissions deserve special caution, because each is effectively cluster-admin in disguise: in a namespace whose ServiceAccounts are privileged (you can mount any Secret and run as any identity); or on roles (you can grant yourself more); and at cluster scope (you can read every credential in the cluster).
ServiceAccounts and their tokens
Every Pod runs as a ServiceAccount — in its namespace unless told otherwise, and should have no permissions at all. Give each workload its own:
Token handling changed and the modern behaviour matters. Since Kubernetes 1.24, creating a ServiceAccount no longer auto-creates a non-expiring Secret token. Pods receive a projected, time-bound, audience-scoped token that the kubelet rotates automatically. For an out-of-cluster consumer, mint one explicitly with rather than creating a long-lived Secret. Long-lived static tokens are exactly the credential that ends up in a CI variable and never gets rotated.
On managed clusters, bind ServiceAccounts to cloud identity (IRSA on EKS, Workload Identity on GKE and AKS) so Pods obtain short-lived cloud credentials without any stored secret.
The least-privilege audit
A concrete, repeatable procedure:
Then: remove every wildcard you cannot justify, replace human cluster-admin with time-bound elevation, delete unused bindings, and re-run quarterly. Tools such as , and automate the reporting.
Pod Security Admission
PodSecurityPolicy was deprecated in 1.21 and removed in Kubernetes 1.25. Its replacement, Pod Security Admission, is a built-in admission controller — stable since 1.25 — configured with namespace labels rather than cluster-wide policy objects.
Three levels: (no restrictions), (blocks known privilege escalations: host namespaces, privileged containers, most host paths), and (baseline plus hardening: run as non-root, drop all capabilities, seccomp , no privilege escalation, restricted volume types). Three modes: (reject), (record in the audit log), (return a warning to the client).
The migration path is exactly that shape: enforce , audit and warn on , fix what the warnings reveal, then raise enforcement. Pin so a cluster upgrade cannot tighten policy underneath you unannounced.
PSA is deliberately limited — it checks Pod security context fields and nothing else. It cannot say "images must come from our registry" or "every Deployment must set resource requests". That needs a policy engine.
Admission control and policy engines
Every request to the API server passes through authentication, then authorisation (RBAC), then admission: first mutating admission (which can modify the object — inject sidecars, set defaults), then schema validation of the mutated object, then validating admission (which can only accept or reject), then persistence.
You extend it three ways:
- MutatingAdmissionWebhook — an HTTPS service you run that patches objects. Powerful, and a liability: if it is down and , the API server rejects the requests it intercepts. Always scope webhooks narrowly with and , and exclude .- ValidatingAdmissionPolicy — in-tree validation expressed in CEL, evaluated by the API server itself, GA since 1.30. No webhook to run or fail. The right first choice for simple rules.- Policy engines — Kyverno (policies are Kubernetes resources written in YAML; validate, mutate, generate and verify image signatures; a gentle learning curve) or OPA Gatekeeper (constraint templates written in Rego; more expressive, steeper curve, shared with non-Kubernetes OPA deployments).
Policies worth having early: require resource requests; require images from an approved registry and by digest (Post 03); forbid tags; require ; require ownership and cost-centre labels; forbid and outside named namespaces. Run everything in Audit mode first — enforcing a policy that most existing workloads violate blocks deploys immediately.
Chapter 5 — Packaging and delivery
Helm
Helm is a package manager: a chart is a directory of Go-templated manifests plus a of defaults; installing it produces a release whose state Helm stores in a Secret in the cluster.
Helm's strengths are real: one command installs a complex third-party system, versioned and parameterised; conditionals and loops express genuine variation; the ecosystem of charts is enormous.
The sharp edges are equally real and worth stating before you commit:
- It is text templating over YAML. Indentation is your problem; and are load-bearing; a template that produces valid text can produce invalid YAML. Error messages point at rendered lines you never wrote.- Charts become unreadable. A generic chart with forty conditionals serving five environments is harder to reason about than five explicit manifests.- Release state lives in the cluster, not in Git. Two operators, two laptops, two versions of the chart, and the state is whatever the last wrote.- is not optional. Without it a failed upgrade leaves the release half-applied. Also set deliberately.- CRDs in are installed but never upgraded or deleted by Helm. Chart upgrades that change a CRD require manual work; this surprises everyone once.- Values sprawl. Deeply nested values files across environments drift, and nothing type-checks them. Schema-validate with .
Kustomize
Kustomize takes the opposite approach: no templating language, only overlays that patch plain YAML. It is built into ().
Every file is valid Kubernetes YAML that your editor, linter and all understand. The hash-suffix behaviour implements the rollout-on-config-change pattern from Post 05 for free. The limits: no conditionals or loops, so genuinely divergent environments mean duplication; deep patch hierarchies get hard to follow; and it does not package or distribute anything.
| | Helm | Kustomize ||---|------|-----------|| Mechanism | Go templating + values | patch plain YAML || Best at | distributing third-party software, wide parameterisation | your own manifests across a few environments || Readability | rendered output differs from source | source is the output || Distribution | repositories, versioned charts, OCI registries | Git directories || State | release history in-cluster | none — the output is applied || Common practice | use Helm for other people's software, Kustomize for your own (or Kustomize's field for both) | |
GitOps
GitOps inverts the direction of deployment. Instead of CI pushing to the cluster with credentials, an agent inside the cluster pulls the desired state from Git and reconciles continuously. Git is the source of truth; the cluster converges on it.
Argo CD models this with an CRD, a strong UI, sync waves and hooks for ordering, and an "app of apps" pattern for managing many. Flux is a set of focused controllers (source, kustomize, helm, image automation, notification) driven entirely by CRDs, with no UI of its own. Both do the same job well; Argo CD suits teams who want the dashboard, Flux suits teams who want everything in the API.
What you actually gain:
- Drift detection and correction. means a manual is reverted within seconds. The cluster cannot silently diverge from the repository.- The audit trail is the Git history. Who changed what, when, reviewed by whom, and how to revert — for free, in a system everyone already knows.- Rollback is . No special tooling, no remembering which Helm revision was good.- Disaster recovery becomes re-pointing an agent at the repository rather than reconstructing state from memory.
And the consequence that matters most: human access to the cluster should shrink once GitOps is in place. If every change arrives through a reviewed pull request, engineers do not need , or in production. Reduce them to read plus , and where justified, with break-glass elevation that is time-bound and alerted on. This is the point at which the RBAC work in Chapter 4 becomes genuinely easy — most people simply do not need write access any more.
Two caveats worth planning for: secrets still cannot be committed in plaintext, so pair GitOps with Sealed Secrets, SOPS, or the External Secrets Operator (Post 05); and image tags in Git must be updated by something — a CI step that commits the new tag, or an image-automation controller — otherwise "GitOps" quietly becomes "someone edits the tag by hand".
Chapter 6 — Managed Kubernetes and cost control
The managed offerings
EKS (AWS), GKE (Google) and AKS (Azure) all run and upgrade the control plane, manage etcd and its backups, and integrate with the cloud's IAM, load balancers and storage. What differs is the amount of remaining work: GKE Autopilot removes node management almost entirely; EKS gives you the most control and the most assembly; AKS sits between them. All charge for the control plane on a per-cluster-hour basis (with a free tier on some), and all charge normally for the worker nodes, load balancers, storage and data transfer — which is where the money actually goes. Check current pricing pages rather than trusting any figure quoted in a blog post, including this one.
The genuinely important decisions are the same everywhere: how many clusters (each has a control-plane cost and an operational cost — resist one per team until you have a reason), what your upgrade cadence is (Kubernetes releases roughly three times a year and support windows are finite), and whether your workloads can tolerate the node churn that spot instances and consolidation require.
Where the money goes, and what to do about it
| Lever | Action | Typical effect ||-------|--------|----------------|| Over-requested resources | right-size from measured usage (Post 06) | usually the single largest win; requests are often multiples of real usage || Idle nodes | Cluster Autoscaler scale-down or Karpenter consolidation | reclaims capacity nobody is using || On-demand everything | spot/preemptible for stateless and batch, with PDBs and graceful shutdown | large discount, in exchange for interruption handling || Oversized instance types | let Karpenter choose per workload rather than one node group for all | better bin-packing || Cross-AZ traffic | topology-aware routing; keep chatty pairs in one zone | inter-zone transfer is metered and adds up quietly || Orphaned resources | audit unbound PVCs, unused LoadBalancers, old snapshots, dangling images | recurring, always finds something || Logs and metrics | retention tiers; drop high-cardinality labels | observability bills can rival compute || Cluster count | consolidate with namespaces + RBAC + quotas instead of new clusters | removes whole control planes |
Make it visible and enforceable: OpenCost or Kubecost attribute spend to namespaces, labels and teams; a and per namespace stop one team consuming the cluster; and mandatory cost-centre labels (enforced by the Kyverno policy in Chapter 4) are what makes attribution possible at all. A monthly review of the top ten namespaces by cost against their requests-versus-usage ratio takes half an hour and consistently pays for itself.
Practice Exercises
Exercise 1 — Ingress or Gateway, end to end
On a local cluster, install a Gateway API implementation and expose two services on one hostname — to one, to another — with TLS from a self-signed issuer. Then express the same routing as an Ingress and list every difference, including which features required controller-specific annotations.
<details><summary>Solution</summary>
</details>
Exercise 2 — Default-deny without breaking DNS
Apply a default-deny NetworkPolicy to a namespace running a two-tier app. Confirm everything breaks. Then restore it with exactly three policies: DNS egress, frontend→backend, backend→database. Verify with a debug Pod that nothing else is reachable.
<details><summary>Solution</summary>
</details>
Exercise 3 — Audit and shrink RBAC
For your own cluster: list every subject bound to , every Role using a wildcard verb or resource, and every ServiceAccount that can read Secrets cluster-wide. Then write a replacement Role for one over-privileged workload and verify with .
<details><summary>Solution</summary>
</details>
Exercise 4 — Debug a shell-less Pod
Build a distroless image with no shell that fails a readiness probe. Diagnose it end to end using only , , , an ephemeral debug container and . Write down which step actually revealed the cause.
Exercise 5 — Policy in audit, then enforce
Install Kyverno. Write a policy requiring CPU and memory requests on every Pod, in mode. Count the violations in your cluster. Fix them, switch to , and confirm a non-compliant Pod is now rejected with a readable message.
Exercise 6 — Helm and Kustomize, same result
Package one application twice: once as a Helm chart with staging and prod values, once as a Kustomize base with two overlays. Render both with and , diff the outputs, and argue which you would choose here and why.
Exercise 7 — GitOps with self-heal
Install Argo CD, point an Application at your Kustomize overlay with and enabled, and confirm it syncs. Then the Deployment by hand and time how long the change survives. Finally, a change and confirm the cluster follows.
Cheat-sheet
| What | How ||------|-----|| One entry point, many services | Ingress or Gateway API — not a LoadBalancer Service per app || ingress-nginx | retired March 2026 — migrate to Gateway API or a maintained controller || Gateway API status | separately installed CRDs; core resources GA, v1.5 promoted more to Standard; Ingress frozen — start new clusters here || Automatic TLS | cert-manager + ClusterIssuer; use the ACME staging endpoint while developing || Pod networking | CNI plugin, flat by default — NetworkPolicy is enforced by the plugin, not by Kubernetes || Default deny | + both , then whitelist — always allow DNS egress; L3/L4 only || Service mesh | real benefits, real cost — most clusters do not need one; require a specific driver and an owner || Metrics stack | Prometheus + node-exporter (nodes) + cAdvisor (containers) + kube-state-metrics (object state); metrics-server is only for and the HPA || Alerting | page on symptoms (error rate, latency, missing output), dashboard the causes || Logs | ship with a node agent; dies with the Pod; log structured JSON with a trace ID || Traces | OpenTelemetry SDK + Collector (DaemonSet + gateway); instrument once, swap backends || First debug moves | → → → ephemeral container || RBAC model | Role/ClusterRole + RoleBinding/ClusterRoleBinding; additive only, no deny || Dangerous grants | cluster-wide , /, in a privileged namespace || ServiceAccount tokens | projected, short-lived, auto-rotated since 1.24; for external use || Pod hardening | Pod Security Admission (PSP removed in 1.25): enforce baseline, audit/warn restricted || Custom policy | ValidatingAdmissionPolicy (CEL, in-tree) or Kyverno / OPA Gatekeeper — start in Audit || Packaging | Helm for other people's software, Kustomize for your own; on every || Delivery | GitOps (Argo CD or Flux): pull, reconcile, , — and shrink human cluster access || Cost control | right-size requests first, then consolidation, spot, cross-AZ traffic and log retention |
Course base complete — what's next
That is the base course. Across eight posts you have gone from a process isolated by namespaces and cgroups on a single host to a multi-node platform with declarative delivery: 00-03 covered what a container actually is, how images are built and layered, networking and volumes and Compose, and image supply-chain security; 04 the reconciliation model, cluster components, Pods, Deployments, Services and DNS; 05 configuration and secrets, the storage stack from Volume to CSI, StatefulSets, DaemonSets, Jobs and CronJobs; 06 scheduling and placement, requests versus limits, QoS and eviction, probes, rollouts, disruption budgets, graceful shutdown and autoscaling; and 07 ingress and the Gateway API, NetworkPolicy, observability, RBAC and admission control, Helm and Kustomize, GitOps and cost.
That is enough to run production services on Kubernetes competently and to read the documentation for anything not covered here without getting lost. The most valuable next step is not another post — it is to operate something real, break it in a non-production cluster on purpose, and practise the failure drills in the exercises until they are boring.
Planned future posts (extensions):
- 08 — Writing a Kubernetes operator — CustomResourceDefinitions, the controller-runtime library, reconciler design, status conditions, finalizers, testing with envtest, and when a CRD is the wrong answer.- 09 — CI/CD pipelines end to end — build, scan, sign, push, promote across environments, image automation, progressive delivery with Argo Rollouts or Flagger.- 10 — Multi-tenancy and platform engineering — namespaces versus clusters, quotas and limit ranges, hierarchical namespaces, vCluster, golden paths, and building an internal developer platform (Backstage, Crossplane).- 11 — Service mesh deep dive — Istio (sidecar and ambient), Linkerd and Cilium mesh compared on identity, traffic policy, telemetry and operational cost.- 12 — Cluster cost engineering and FinOps — OpenCost and Kubecost, showback and chargeback, spot strategies, bin-packing, and measured before/after case studies.- 13 — EKS in production — VPC CNI and IP exhaustion, IRSA and Pod Identity, node groups versus Karpenter, add-on lifecycle and upgrades, cross-linking the existing AWS series (, , , ).- 14 — Stateful data on Kubernetes, deep — CloudNativePG, Strimzi and Vitess in practice; backup, point-in-time recovery, failover drills and storage performance measurement.- 15 — Disaster recovery, upgrades and cluster lifecycle — etcd backup and restore, version skew policy, blue/green cluster upgrades, Velero, and running a real DR exercise.
References
- Ingress · Ingress controllers · Gateway API- Gateway API v1.5: Moving features to Stable · Ingress NGINX: statement from the Steering and Security Response Committees · Announcing ingress2gateway 1.0- cert-manager · Network Policies · Cilium · Calico- Prometheus · kube-state-metrics · OpenTelemetry · Google SRE Book — Alerting on SLOs- RBAC · ServiceAccounts · Pod Security Admission · Admission controllers- Helm · Kustomize · Argo CD · Flux · Kyverno · OPA Gatekeeper · OpenCost- — OpenTelemetry instrumentation and trace correlation in depth.
> Note: this post — and this base course — is in progress. Planned additions: a full Gateway API migration walkthrough from a real ingress-nginx configuration; a Kyverno policy pack covering the Post 03 supply-chain controls; an Argo CD app-of-apps reference repository; a measured cost case study before and after right-sizing.
Containers Course — 06 · Scheduling, Resources, Probes and Rollouts
Posts 04 and 05 covered what to declare. This post covers how it behaves under pressure: which node a Pod lands on, how much CPU and memory it may consume, what happens when it wants more than it asked for, how Kubernetes decides whether it is healthy, and how a new version replaces an old one without dropping requests. Almost every self-inflicted Kubernetes outage — cascading restarts, mysterious 502s during deploys, nodes that evict the wrong workloads, autoscalers that oscillate — traces back to one of the settings in this post.
> Series map: 00 Containers from First Principles → 01 Images and Dockerfiles → 02 Networking, Volumes and Compose → 03 Registries, Supply Chain and Security → 04 Kubernetes Architecture and Core Objects → 05 Config, Storage and Stateful Workloads → 06 Scheduling, Resources, Probes and Rollouts (this post) → 07 Ingress, Observability, RBAC and GitOps.
Learning Outcome
- Describe the scheduler's filter-then-score cycle and predict why a Pod is .- Choose correctly between nodeSelector, node affinity, pod anti-affinity, taints and topology spread constraints.- Explain the single most consequential idea in Kubernetes resource management: requests drive scheduling, limits drive enforcement.- Determine a Pod's QoS class, predict its eviction order, and recognise an OOMKill from exit code 137.- Argue coherently about CPU limits and CFS throttling, and set memory limits without causing restarts.- Configure liveness, readiness and startup probes so they cannot cause the outage they are meant to prevent.- Run a rolling update that neither drops traffic nor breaks quorum, using surge settings, PodDisruptionBudgets, and grace periods.- Configure an HPA that converges instead of oscillating, and right-size resources from measured data.
Chapter 1 — How the scheduler works
Terminology
- Scheduling — assigning an unscheduled Pod to a node by writing . The kubelet on that node then starts it.- Filter (predicate) — a check that eliminates nodes a Pod cannot run on.- Score (priority) — a ranking of the surviving nodes by how good a fit they are.- Binding — persisting the chosen node back to the API server.- Feasible node — a node that survived every filter.
The two phases
For each Pod in the scheduling queue, runs a scheduling cycle and then a binding cycle. The scheduling cycle has two halves:
1. Filter. Every node is tested against a series of predicates. Any failure removes the node from consideration:
- Does the node have enough allocatable CPU, memory and ephemeral storage to satisfy the Pod's requests?- Do the Pod's and required node affinity match the node's labels?- Are the node's taints tolerated by the Pod?- Are the requested host ports free?- Can the Pod's volumes be attached here (zone, node affinity of the PV, attach limits)?- Do required pod affinity and anti-affinity rules hold?- Is the node and not under , or ?
If zero nodes survive, the Pod stays and the scheduler records an event naming the failing predicate — . That event is the answer to almost every "why is my Pod Pending" question.
2. Score. Each feasible node is scored 0-100 by several plugins and the totals are weighted:
- — spread load (, the default) or pack it tight (, better for autoscaler-driven cost savings).- — prefer nodes that already have the image cached.- , , , — the preferred (soft) forms of each rule.
The highest-scoring node wins; ties are broken at random. The scheduler then reserves the resources, runs any plugins, and binds asynchronously.
Two properties matter operationally:
- The scheduler decides once, at admission. It does not move a running Pod because the cluster later became unbalanced. Rebalancing requires deleting Pods (the project automates this).- The scheduler reasons about requests, never about actual usage. A node can be 95 % idle and still reject a Pod because its requests are fully committed — and it can be 100 % CPU-saturated and happily accept more Pods whose requests fit.
> Trick: prints an "Allocated resources" table with requests and limits as a percentage of allocatable. That table, not , is what the scheduler sees.
Chapter 2 — Controlling placement
nodeSelector — the simple case
Exact-match labels only, hard requirement. Fine for small, unambiguous rules; it cannot express preferences, negation, or set membership.
Node affinity — the expressive case
Read the long field names literally. is a filter; is a score contribution. is the important half: an already-running Pod is never evicted because the node's labels changed. There is no — if you need eviction on a condition change, use a taint with .
Pod affinity and anti-affinity
These place Pods relative to other Pods, using a to define what "together" means.
> Common pitfall: hard on caps your replica count at the node count. Scale to 10 replicas on a 6-node cluster and 4 Pods sit forever. Use the preferred form, or topology spread constraints, unless the co-location really is unacceptable.
Pod affinity is also computationally expensive — the scheduler must evaluate every Pod in the matched topology for every candidate node. On large clusters, prefer topology spread constraints.
Taints and tolerations — the inverse
Affinity is a Pod attracting a node. A taint is a node repelling Pods. Only Pods with a matching toleration may land there.
| Effect | Meaning ||--------|---------|| | no new Pod without a toleration || | soft version — avoid if possible || | as , plus evict already-running Pods that do not tolerate it (optionally after ) |
Taints are how you reserve expensive hardware (GPU nodes, high-memory nodes) for the workloads that need it, how control-plane nodes stay free of user workloads, and how the node lifecycle controller cordons off unhealthy nodes — and are taints applied automatically.
Note that a toleration does not attract. Tolerating the GPU taint lets a Pod run on GPU nodes; it does not make it prefer them. Pair a toleration with node affinity to get both halves.
Topology spread constraints — the modern way to spread
is the maximum permitted difference between the most-populated and least-populated domain. With across three zones, nine replicas land 3/3/3 and ten land 4/3/3. This expresses "spread evenly" directly, instead of approximating it with anti-affinity, and it is far cheaper for the scheduler to evaluate.
The standard production pairing for a stateless service: a hard zone spread with and a soft hostname spread. That survives an availability-zone loss and still schedules when a zone is temporarily full.
> Watch out: combined with a cluster autoscaler can deadlock — the Pod is unschedulable, so the autoscaler adds a node, but the new node is in the wrong zone, so the Pod remains unschedulable. Verify that your node groups actually span the topology you are spreading over.
Chapter 3 — Requests, limits and QoS
The one sentence that matters
Requests drive scheduling. Limits drive enforcement.
- A request is a reservation. The scheduler subtracts it from the node's allocatable capacity and guarantees the Pod that much. It is also the number that decides whether the Pod fits at all.- A limit is a ceiling enforced at runtime by the kernel through cgroups. Exceed the CPU limit and you are throttled. Exceed the memory limit and you are killed.
Everything else in this chapter follows from that distinction.
CPU is compressible: exceeding your share means running slower. Memory is incompressible: there is no "slower", only "killed".
QoS classes
The kubelet assigns each Pod a class based purely on how its requests and limits are written:
| Class | Condition | Consequence ||-------|-----------|-------------|| Guaranteed | every container sets both CPU and memory, and request == limit for each | evicted last; eligible for exclusive CPUs under the static CPU manager policy || Burstable | at least one request is set, but not the Guaranteed pattern | evicted after BestEffort, in order of how far usage exceeds requests || BestEffort | no requests or limits anywhere in the Pod | evicted first, always |
Under node memory pressure the kubelet evicts in that order: BestEffort first, then Burstable Pods that are using more than they requested (ranked by the excess), then Guaranteed. The kernel's own OOM killer uses a parallel mechanism, , derived from the same classes.
> Common pitfall: BestEffort Pods look thrifty and are actually hazardous. They are the first thing evicted when a node fills up, they contribute nothing to autoscaler capacity calculations, and they can starve neighbours. Set requests on everything, including the small "unimportant" jobs.
CPU limits and CFS throttling
A CPU limit is implemented as a CFS quota: the cgroup is granted quota microseconds of CPU time per period (100 ms by default). A limit of means 50 ms of CPU per 100 ms window. When the quota is exhausted, every thread in the cgroup is stopped until the next period begins.
This produces a failure mode that is invisible in average CPU graphs. An application averaging 30 % of its limit can still be throttled hard, because the work is not spread evenly: a request arrives, several threads wake, the quota is consumed in 15 ms, and the container sleeps for the remaining 85 ms. The result is tail latency in whole multiples of 100 ms while the dashboard shows a comfortable average.
Multithreaded runtimes make it worse. A JVM or Go program that sees 32 host CPUs will happily run 32 threads, burning the quota 32× faster than a single thread would.
Measure it — do not guess:
Sustained values above roughly 5-10 % on a latency-sensitive service are worth acting on. Confirm on your own workload rather than trusting any published threshold.
The genuine argument for omitting CPU limits
Set memory limits always; consider omitting CPU limits on latency-sensitive services.
The case for omitting: CPU is compressible and already fairly shared. When several containers on a node contend, the kernel divides CPU time in proportion to their requests (cgroup CPU shares) — so requests alone already provide isolation under contention. A limit adds nothing during contention and actively hurts when the node is idle, because it forbids a container from using spare capacity it could have had for free. Throttling a service that could have run faster, on a machine that is 60 % idle, is pure loss.
The case for keeping them: limits make behaviour reproducible and capacity predictable, they stop one runaway process from degrading its neighbours' latency (fair sharing is fair, but a busy neighbour still slows you down), some regulated or multi-tenant environments require hard ceilings, and Guaranteed QoS — which requires a CPU limit — is needed for exclusive CPU pinning.
A defensible policy: memory limit always set and equal to a measured peak plus headroom; CPU request set from measured usage; CPU limit omitted for latency-sensitive services, set generously (well above the request) for batch and untrusted workloads. Whatever you choose, also set (Go) or the container-aware JVM flags so the runtime sizes its thread pools to the cgroup, not to the host.
OOMKill and exit code 137
Exceed the memory limit and the kernel's OOM killer terminates the process with . The container exits with 137 — the shell convention of , and is SIGKILL.
Diagnosis:
- Exit 137 with — the container exceeded its own memory limit. Raise the limit or fix the leak.- Exit 137 without — something sent SIGKILL: usually a grace period expiring during shutdown (Chapter 5), or a node-level OOM.- Pod status — the node ran out of memory and the kubelet chose this Pod by QoS ranking. Different problem: the node is over-committed.- Exit 143 is (SIGTERM) — a normal, graceful termination.
Note also that a request far below the real working set is what makes node-level OOM possible in the first place: the scheduler packed the node believing the Pods were small.
Chapter 4 — Probes
Three probes, three completely different jobs. Confusing them is the most common cause of self-inflicted outages in Kubernetes.
| Probe | Question | Action on failure ||-------|----------|-------------------|| liveness | "Is this process wedged beyond recovery?" | kill and restart the container || readiness | "Should this Pod receive traffic right now?" | remove the Pod from Service endpoints (no restart) || startup | "Has the process finished booting?" | kill the container; while it runs, liveness and readiness are suspended |
The classic outage
A team writes one endpoint that checks the database, and wires it to both the readiness and the liveness probe. The database has a 30-second hiccup.
1. Every replica's liveness probe fails.2. The kubelet kills and restarts every replica simultaneously.3. The database recovers — and is immediately hit by the full fleet reconnecting and re-warming caches at once.4. The probes fail again. Restart storm. . The blast radius of a 30-second dependency blip is now a total outage that outlives it by many minutes.
The rule that prevents it: a liveness probe must never test anything outside the process. If the dependency is down, restarting your container does not help — it makes recovery harder by discarding warm state and multiplying reconnection load.
- Liveness → shallow. "Is the event loop running, can I serve a trivial response?" Many mature services are better off with no liveness probe at all; a process that crashes on a fatal fault is restarted by the Pod restart policy anyway.- Readiness → may be deep. Failing readiness removes the Pod from load balancing and is reversible with no state loss. If every replica fails readiness the Service has no endpoints and clients get connection errors — bad, but strictly better than a restart storm, and it recovers the instant the dependency does.- Startup → the correct tool for slow-starting applications. Without it you must inflate or on liveness, which weakens liveness forever to accommodate one slow boot.
Timing parameters
| Field | Default | Guidance ||-------|---------|----------|| | 0 | prefer a startup probe over a large value here || | 10 | 5-10s for readiness, 10-20s for liveness || | 1 | raise it — 1 second is shorter than many healthy responses under load || | 3 | liveness: should exceed your worst legitimate GC pause or lock wait || | 1 | must be 1 for liveness and startup || | inherited | can be overridden per probe for the liveness-triggered kill |
> Watch out: the default is the quiet cause of many phantom restarts. Under load, a healthy service can take longer than a second to answer, and three such answers in a row become a restart.
Probe types are , , and (the gRPC health-checking protocol, GA since 1.27). Prefer or ; probes fork a process on every check and are surprisingly expensive at scale.
Chapter 5 — Rollouts, disruption and graceful shutdown
Rolling updates
- — the safe default. Capacity never dips; the rollout needs spare cluster capacity and is slightly slower.- — for workloads that cannot be over-provisioned (a licence limit, a singleton lock). Capacity dips during the roll.- is underused and valuable: it stops a rollout that passes readiness and then dies five seconds later from marching all the way through the fleet.- marks the Deployment when it stalls, which is what your alerting and your GitOps controller should watch. Note that it does not roll back automatically.
bounds how far back can go, at the cost of keeping that many zero-replica ReplicaSets. Blue/green and true canary deployments need more than a Deployment provides — Argo Rollouts, Flagger, or a service mesh (Post 07).
Voluntary versus involuntary disruption
| Involuntary | Voluntary ||-------------|-----------|| node hardware failure, kernel panic | for an upgrade || node OOM, kubelet eviction | cluster autoscaler removing an under-used node || spot / preemptible instance reclaim | a rolling update you triggered || a container OOMKilled | deleting a Pod by hand |
A PodDisruptionBudget constrains only the voluntary column. It works by making the Eviction API refuse an eviction that would violate the budget; anything that does not go through that API — a node catching fire — is unaffected.
Guidance:
- Use for large stateless fleets — it scales with replica count without editing.- Use for quorum systems (etcd, Zookeeper, a 3-member database): losing quorum during a routine node drain is a real and common outage.- A PDB that can never be satisfied blocks node drains indefinitely. on a Deployment with means no Pod may ever be evicted, and every cluster upgrade hangs. Always leave slack.- A PDB on a single-replica Deployment is a contradiction: you are asserting that the Pod is both critical and unreplicated.
Graceful shutdown and the endpoint-removal race
When a Pod is deleted, two things happen concurrently, and this is the subtle part:
Path B is a distributed propagation across every node in the cluster; path A is local and immediate. So a container can receive SIGTERM, stop accepting connections, and still be sent traffic for hundreds of milliseconds to several seconds afterwards, by proxies that have not yet heard the news. That is the source of the 502s and connection resets that appear during otherwise "zero-downtime" deploys.
The fix is to make the container keep serving for a moment after SIGTERM:
runs before SIGTERM and the grace period clock covers both, so the grace period must exceed the sleep plus the real drain time. The sequence you want:
1. sleeps 5-15 s. The container is still healthy and still serving; meanwhile every proxy learns the Pod is gone.2. SIGTERM. The application stops accepting new connections, finishes in-flight requests, closes pools, flushes buffers, and exits.3. If it has not exited by (default 30), SIGKILL — exit code 137, in-flight work lost.
Requirements that are easy to miss:
- The application must actually handle SIGTERM. Many do not, and rely on being killed. Check.- PID 1 must receive the signal. A container started via makes the shell PID 1, and it does not forward signals. Use exec form (), an init such as , or semantics. This was covered in Post 01 and it bites hardest here.- Set the grace period from measurement. A worker draining 5-minute jobs needs a grace period longer than 5 minutes, or every deploy kills work in flight.
> Trick: if you cannot add a binary to a distroless image, use against an endpoint that flips readiness to false and blocks, or add a implementation to the image explicitly. The pattern matters more than the mechanism.
Chapter 6 — Autoscaling
HorizontalPodAutoscaler
The HPA changes on a Deployment or StatefulSet to keep a metric near a target. Use — v1 supported CPU only.
Essential facts:
- The algorithm is , with a tolerance band (10 % by default) so small deviations do not cause churn.- is a percentage of the CPU request, not of the node or the limit. A Pod requesting and using is at 90 % utilisation regardless of how idle the node is. An HPA on a workload with no CPU request cannot work at all.- makes the controller use the maximum recommendation over the window when scaling down (and the minimum when scaling up), which is what stops flapping. Asymmetric windows — scale up fast, scale down slowly — are almost always right.- Resource metrics need metrics-server installed. Custom and external metrics need an adapter (typically Prometheus Adapter or KEDA).- Never point an HPA and a VPA at the same metric on the same workload. They fight: the VPA raises the request, which lowers utilisation, which makes the HPA scale in, which raises per-Pod load, which makes the VPA raise the request again.- The HPA cannot scale below 1 without the alpha/beta scale-to-zero feature; KEDA is the usual answer for event-driven scale-to-zero.
VerticalPodAutoscaler
The VPA recommends — and optionally applies — better CPU and memory requests, learned from observed usage. Modes: (recommend only), (apply at Pod creation), / (evict and recreate Pods to apply new values).
mode is genuinely useful on its own: run it across the cluster and read as a free, continuously updated right-sizing report, without letting it touch anything. Historically mode had to evict Pods to change resources, which made it disruptive; in-place Pod resize graduated to stable in Kubernetes 1.35, and pod-level in-place resize reached beta in 1.36, so on current clusters resource changes can increasingly be applied without a restart. Check what your cluster version and your VPA release actually support before relying on it.
Cluster Autoscaler and Karpenter
The HPA adds Pods. Something has to add nodes when those Pods do not fit.
| | Cluster Autoscaler | Karpenter ||---|---|---|| Model | scales pre-defined node groups / ASGs up and down | provisions individual nodes chosen to fit the pending Pods || Instance choice | fixed per node group | picks instance type, size, zone and capacity type per batch of Pods || Speed | minutes (ASG-mediated) | typically faster; talks to the cloud API directly || Consolidation | scale-down of under-used nodes | active consolidation and bin-packing; replaces nodes with cheaper ones || Availability | all major clouds | AWS-native origin, now with other providers |
Both react to unschedulable Pods, which means both depend entirely on your requests being accurate. Requests that are too high make the autoscaler buy capacity nobody uses; requests that are too low let it under-provision and then evict. Note also that Karpenter-style consolidation deliberately churns nodes, so PodDisruptionBudgets and correct graceful shutdown (Chapter 5) stop being optional.
Right-sizing methodology
A repeatable procedure, in order:
1. Instrument first. Without and a metrics store you are guessing. Collect at least two weeks, covering a weekly cycle and any peak event.2. Set memory request at P95 of observed working set; set the memory limit at the observed peak plus 25-50 %. Memory is incompressible, so the limit is a safety fuse, not a target.3. Set CPU request at P90 of observed usage. This is the scheduling reservation and the HPA's denominator, so it should reflect typical, not peak.4. Decide the CPU limit by policy (Chapter 3), then verify with the throttling ratio.5. Check the QoS class you ended up with — — and confirm it matches the workload's importance.6. Compare requests against actual usage cluster-wide. The ratio of total requests to total usage is your headline waste number, and it is usually far worse than anyone expects.7. Re-measure after every significant release. Right-sizing is not a one-off project.
Read those two together: a memory ratio consistently near 1.0 predicts OOMKills; a CPU ratio consistently near 0.05 is money on fire.
Practice Exercises
Exercise 1 — Diagnose a Pending Pod
Create a Deployment requesting more CPU than any node can provide, then one with an untolerated taint, then one with hard anti-affinity and more replicas than nodes. For each, find the exact scheduler message and explain which predicate failed.
<details><summary>Solution</summary>
</details>
Exercise 2 — Provoke and identify an OOMKill
Run a container with that allocates 200 MiB. Capture the exit code, the termination reason and the restart count. Then set the request to with no limit and compare what happens when the node runs out of memory instead.
<details><summary>Solution</summary>
</details>
Exercise 3 — Reproduce the liveness cascade
Deploy three replicas whose endpoint returns 500 when a dependency Service is unreachable, wired to the liveness probe. Scale the dependency to zero. Record how long until all three are in . Then split the endpoint into a shallow for liveness and a deep for readiness and repeat.
<details><summary>Solution</summary>
</details>
Exercise 4 — Measure the endpoint-removal race
Put a load generator at steady RPS against a Service. Roll the Deployment with no hook and count failed requests. Add and a grace period of 60 s, roll again, and compare. Report both numbers.
Exercise 5 — PDB versus node drain
Create a 3-replica Deployment with , then its node. Observe the drain block. Fix it two ways — relax the PDB, and add capacity — and explain which you would use during a real cluster upgrade.
Exercise 6 — Tune an HPA that oscillates
Configure an HPA with on both directions and drive it with a spiky load. Record the replica count over 15 minutes. Then apply asymmetric behaviour (fast up, 10-minute down window) and compare replica churn and p99 latency.
Exercise 7 — Right-size a real workload
Pick one Deployment. Pull 7 days of CPU and memory usage. Compute P90 CPU and P95 memory, set requests accordingly, set the memory limit at peak + 30 %, and measure the throttling ratio before and after. Report the change in requested capacity and in p99 latency.
Cheat-sheet
| What | How ||------|-----|| Scheduler cycle | filter (feasible nodes) → score (best node) → bind || Why is my Pod Pending | events name the failing predicate || Scheduler sees | requests, never live usage — shows the same table || Never co-locate replicas | on — use preferred, or you cap replicas at node count || Spread evenly | , , zone (hard) + hostname (soft) || Repel Pods from a node | taint ( also evicts); a toleration permits but does not attract || The core rule | requests drive scheduling, limits drive enforcement || QoS | Guaranteed (request == limit everywhere) → Burstable → BestEffort (evicted first) || CPU limit | CFS quota per 100 ms period; measure || Policy | always set memory limits; consider omitting CPU limits on latency-sensitive services || Exit 137 / 143 | SIGKILL (, or a grace-period kill) / SIGTERM, a normal stop || Liveness probe | shallow only; never check a dependency, or one blip restarts the fleet || Readiness probe | may be deep; failing it removes endpoints without losing state || Startup probe | for slow boots, instead of inflating liveness thresholds || Probe timeout | default is too low — raise it || Safe rollout | , , ; wait with || PDB scope | voluntary disruption only; never satisfiable ⇒ drains hang forever || Drop-free deploys | + grace period > drain time + SIGTERM actually handled || HPA | ; utilisation is a % of the request; window 0, 300-600 s || Right-sizing | memory request ≈ P95, memory limit ≈ peak + 25-50 %, CPU request ≈ P90 |
What's next
Post 07 closes the base course with Ingress, Observability, RBAC and GitOps — getting external traffic in through an ingress controller or the Gateway API, NetworkPolicy for default-deny east-west traffic, the metrics/logs/traces triad and what to alert on, RBAC and Pod Security Admission, admission control and policy engines, Helm versus Kustomize, and continuous delivery with Argo CD or Flux.
References
- Kubernetes Scheduler · Assigning Pods to Nodes · Taints and Tolerations- Pod Topology Spread Constraints- Resource Management for Pods and Containers · Pod QoS classes · Node-pressure eviction- Configure Liveness, Readiness and Startup Probes- Pod lifecycle — termination · Disruptions- HorizontalPodAutoscaler · Vertical Pod Autoscaler · Cluster Autoscaler · Karpenter- Kubernetes 1.35: In-Place Pod Resize Graduates to Stable
> Note: this post is in progress. Planned additions: a measured CFS-throttling experiment with before/after latency histograms; a descheduler walkthrough for rebalancing after node changes; an Argo Rollouts canary example with automated analysis; a KEDA event-driven scale-to-zero recipe.
JavaScript Deep Dive — 02 · Objects, Prototypes & Metaprogramming
Most JavaScript bugs that survive code review are not algorithm bugs. They are object-model bugs: a callback that lost its , a class field that silently shadowed a base-class setter, a "deep copy" that was one level deep, a that broke private fields, a cache that kept every request alive. Each of these follows directly from rules in the specification that are short, precise and rarely taught in full. This post states those rules: environments and the temporal dead zone, the four bindings, the prototype chain and what really builds, private fields and static blocks, property descriptors, symbols and the iteration protocols, and with their invariants, weak references, and the differences between , spread and JSON copies.
> Series map: 00 Engines & Runtime → 01 Event Loop & Concurrency → 02 Objects, Prototypes & Metaprogramming (this post) → 03 Async Engineering → 04 Modules, Packaging & TypeScript at Scale → 05 Performance, Memory & Production Debugging. Future: see the extensions table in 05.
Learning Outcome
- Explain scopes as environment records, and state exactly when the temporal dead zone (TDZ) applies.- Recognise the closure-retention leak caused by shared scope contexts.- Apply the four rules in order of precedence, plus arrow functions and class fields.- Draw the prototype chain for a class hierarchy, including the constructor-side chain.- Use private , brand checks, static blocks and accessors, and know how class fields differ from assignments.- Read and write property descriptors; choose between , and .- Implement the iterator protocol, use generators for lazy pipelines, and hook into well-known symbols.- Build a that forwards correctly with , and respect its invariants.- Choose between , and , and between , spread and JSON for copies.
Chapter 1 — Scope, closures and the TDZ
Terminology
- Environment record — the specification's name for a scope: a map from names to bindings. Function calls, blocks, modules and the global scope each create one. Each record points to its outer record; name lookup walks that chain.- Binding — a named slot. and function declarations create bindings that are initialised to (or the function) when the scope is entered. , and create bindings that stay uninitialised until their declaration runs.- TDZ (temporal dead zone) — the time between entering a scope and executing a // declaration. Any access to the binding in that window throws .- Closure — a function object plus a reference to the environment where it was created.
The TDZ, precisely
The TDZ is temporal, not textual: what matters is when the access runs, not where it is written.
Other TDZ cases that bite in real code: a used before its declaration in the same module (classes are not hoisted like functions), and circular ES module imports, where a module reads an imported before the exporting module has evaluated it — the error message names the binding, not the cycle.
Closures capture bindings, not values
The shared-context leak
V8 (like other engines) does not give each closure its own copy of the variables it uses. Variables captured by any closure in a scope go into one context object for that scope, and every closure created in that scope keeps the whole context alive:
Keeping 20 of the returned closures alive retained about 164 MB of heap in Node 22; the same code without retained about 4 MB. Real versions of this leak involve an event handler or a timer callback created in the same function as a closure that touched a large response or DOM tree.
> Trick: when a long-lived callback is created in a function that also handles big data, move the callback's creation into a small separate function that receives only what it needs. The new scope has its own context, and the big object is no longer reachable.
Chapter 2 —
is not lexical (except in arrow functions). It is decided at call time, by how the function is called. In order of precedence:
| Rule | Call shape | is ||---|---|---|| 1. | | The newly created object (a bound function's bound is ignored) || 2. Explicit | , , | || 3. Implicit (method) | , | — the object before the dot, at the moment of the call || 4. Default | | in strict code (modules and classes are always strict); in sloppy scripts || Arrow functions | any | The of the enclosing scope; / cannot change it |
Where it goes wrong in practice:
- Passing methods as callbacks — , , . For DOM listeners becomes the element; for the others, .- Destructuring methods — works only because methods happen not to rely on in modern engines; does not.
Fixes, with their trade-offs:
> Watch out: arrow-function class fields are convenient, but they live on each instance, not on the prototype. They cannot be overridden with in a subclass, do not appear on the prototype for mocking in tests, and cost one closure per instance — noticeable for classes instantiated thousands of times.
Chapter 3 — The prototype chain
Every object has an internal slot — another object or . Reading a property walks the chain until it finds an own property with that key; writing does something subtler.
What builds
There are two chains:
- Instance side: → (has ) → (has ) → → .- Constructor side: → → → → . This is why works: static methods are inherited because .
Facts that follow from the spec and matter in code review:
- Class methods are non-enumerable, so they never show up in , or spread.- Class bodies are strict mode, and calling a class without throws .- is resolved through the method's home object (the object it was defined on), not through , so it keeps working when the method is borrowed.- checks whether appears on the chain (unless defines ). It fails across realms — an array from an iframe is not in the parent. Prefer , or brand checks (Chapter 4).
Writing is not symmetric with reading
does not simply create an own property. If is found on the prototype chain:
| Found on the chain as | Result of ||---|---|| Nothing | Creates an own data property on || A writable data property | Creates an own property on (shadowing) || A non-writable data property | Assignment fails: in strict mode, silently ignored in sloppy mode. No own property is created || An accessor (setter) | Calls the inherited setter with ; no own property is created |
This asymmetry between assignment (, follows the chain) and definition (, does not) is the key to class fields in the next chapter.
> Common pitfall: using a plain object as a dictionary of user-supplied keys. hits the inherited accessor and changes the object's prototype instead of storing a key — the root of many prototype-pollution vulnerabilities. Use , or for a chain-free object.
Chapter 4 — Classes in depth
Fields are defined, not assigned
Class fields use define semantics (like ), not assignment:
The own property now shadows the base-class accessor. Code migrated from constructor assignments (, which would call the setter) to fields changes behaviour. TypeScript follows the standard when is (the default for targets ES2022 and later); older configurations emitted assignments, so upgrading the target can change behaviour for exactly this pattern.
Private members and brand checks
Private names are lexically scoped to the class body and are not properties at all: they are not reachable by , , traps or . Accessing on an object that lacks it throws — which makes a reliable, realm-independent brand check, unlike .
> Watch out: private fields and do not mix. A method called through a proxy has , and the proxy object has no private fields:>>>> Any library that wraps your objects in proxies (Vue's is the best-known example) hits exactly this. Either keep such classes out of proxied state, or have their methods work on the raw target.
Chapter 5 — Property descriptors and integrity levels
Every own property has a descriptor. Data properties have and ; accessor properties have and ; both have and .
| Created by | | | ||---|---|---|---|| Assignment or object literal | | | || Class field | | | || Class method | | | || with fields omitted | | | |
The last row is a frequent surprise: creates a read-only, hidden, permanent property. then returns .
means the property cannot be deleted and its descriptor can no longer change — with one exception: a writable data property may still be made non-writable.
Integrity levels
| | Add properties | Delete | Reconfigure | Change values ||---|---|---|---|---|| | No | Yes | Yes | Yes || | No | No | No | Yes || | No | No | No | No (data properties) |
All three are shallow: succeeds. Freezing also does not stop accessor properties from changing internal state. A deep freeze must recurse, and it should be a development-time guard (freezing Redux state in dev builds is a common use), not a security boundary.
Chapter 6 — Symbols, iterators and generators
Symbols
A is a unique, non-string property key. ; returns a shared, registry-wide symbol (the same across realms). Symbol-keyed properties are skipped by , and , but are copied by spread and , and listed by . They are hidden from casual enumeration, not private.
Well-known symbols are the engine's hooks into your objects:
| Symbol | Hook | Used by ||---|---|---|| | Return an iterator | , spread, destructuring, , || | Return an async iterator | || | Convert to a primitive, given a hint (, , ) | , template literals, || | Custom | || | Tag for | in logs and type checks || | Constructor for derived objects | / on subclasses, on subclasses || , | Cleanup hook | / declarations (explicit resource management; shipped in Chrome 134 and Node 24, check other engines; Post 03) |
The iteration protocols
An iterable has a method returning an iterator: an object with returning , and optionally . calls when the loop exits early (, , ) — this is how iterators release resources.
Generators
A generator function returns an object that is both iterable and an iterator, with the protocol implemented for you. It runs lazily, one at a time, and its blocks run when a consumer stops early:
Since ES2025, iterator helpers give every built-in iterator lazy , , , , , , , , , and , and wraps any iterable:
Nothing is computed until pulls values, so an infinite source is fine. Unlike array methods, no intermediate arrays are allocated.
> Trick: delegates to another iterable and forwards and to it, so composed generators still clean up correctly. Hand-written iterator wrappers usually forget to forward and leak the inner resource.
Chapter 7 — and
A wraps a target and intercepts the object's internal methods through traps: , , , , , , , , , , , and for functions and . has a method with the same name and signature for every trap, performing the default behaviour.
Forward with , and pass the receiver
The argument is the object the operation started on. Forwarding it is what makes getters, setters and inheritance (a proxy used as a prototype) behave correctly.
Invariants
A proxy may lie, but not about facts the target has made permanent. The engine checks each trap's result against the target and throws on a violation. The main ones:
- must return the actual value for a non-writable, non-configurable own data property of the target.- and cannot hide a non-configurable property, or any property of a non-extensible target.- must include every non-configurable own key; for a non-extensible target it must list exactly the target's keys.- must match the target; must match for a non-extensible target.
This is why you cannot make a "read-only view" of a frozen object that rewrites values, and why reactive systems proxy mutable targets.
Costs and limits
- Identity: . Sets, maps and checks see two different objects; libraries keep a from target to proxy to return the same proxy each time.- Internal slots: built-ins keep data in internal slots that a proxy does not have. throws ( called on an incompatible receiver); the same applies to , , typed arrays and private fields.- Speed: every trapped operation is a function call, and proxied access is much slower than a plain property load in optimised code (Post 00's inline caches cannot see through a trap). Keep proxies at boundaries — validation, reactivity, API clients — not in hot inner loops.- returns ; after , every operation on the proxy throws. Useful for handing out capabilities that can be withdrawn.
Chapter 8 — Weak references and copying
Weak collections
| | Keys / target | Enumerable | Typical use ||---|---|---|---|| | Objects and non-registered symbols | No | Per-object metadata and caches that die with the object || | Same | No | Marking objects ("already visited", "already wrapped") || | One object | — | A cache entry that may disappear; returns the object or || | Callback after an object is collected | — | Releasing an external resource as a backstop |
A entry keeps its value alive only while the key is alive — the right structure for "extra data about objects I do not own" (DOM nodes, request objects, library instances):
and are deliberately weak guarantees. The specification allows a finalization callback to run late or never (for example, when the page closes). Within one synchronous job, keeps returning the same object (the engine keeps it alive until the job ends), so check once and use the result. Never put essential cleanup — closing a file, releasing a lock — in a finalizer; use or , and treat the registry as a leak detector or a last-resort backstop.
Copying: vs spread vs JSON
| | Spread / | | ||---|---|---|---|| Depth | Shallow | Deep | Deep || | Same object shared | Becomes a string | || , | Same object shared | Becomes | , || values | Kept | Key dropped | Kept || , | Kept | Become | Kept || | Kept | Throws | Kept || Circular references | Shared (shallow) | Throws | Preserved || Functions | Copied by reference | Dropped | || Getters | Invoked; value copied | Invoked; value copied | Invoked; value copied || Prototype / class | Lost (plain object) | Lost | Lost || Symbol keys | Copied | Dropped | Dropped || Private | Not copied | Not copied | Not copied |
(Cells checked in Node 22.) The practical rules:
- Spread for one level of immutable update (), which is what React and Redux reducers need.- for a real deep copy of data — including , , , typed arrays, cycles.- Neither preserves behaviour: class instances come back as plain objects. To copy an instance, give the class its own method.- JSON round-trips are for serialisation, not copying.
Practice Exercises
Exercise 1 — TDZ or not?
Which lines throw?
<details><summary>Solution</summary>
Only (b) throws : is declared with in the same scope and is still in the TDZ. (a) prints — is safe for names that are not declared at all. (c) works: is read when runs, after its initialisation. The TDZ is about time, not position.
</details>
Exercise 2 — What is ?
What do , , and do?
<details><summary>Solution</summary>
returns 1 (implicit binding). throws ( is in strict code, so fails). logs : the callback is a plain function called by the timer (in Node is the object; in browsers it is ), neither of which has . logs the current , because the arrow takes from , which was called as a method.
</details>
Exercise 3 — The setter that stopped firing
A base class has . A subclass declares as a field. Validation stopped running after a TypeScript target upgrade to ES2022. Why, and what are two fixes?
<details><summary>Solution</summary>
With (default for ES2022+ targets), the field is defined as an own data property on the instance, which shadows the inherited accessor; the setter never runs, now or on later assignments. Fixes: (1) remove the field and assign in the constructor (), which goes through and calls the setter; or (2) use TypeScript's if the field was only there for typing, which emits nothing.
</details>
Exercise 4 — A logging proxy that works
Write returning a proxy that logs every property read and write, works with getters and setters that use , and returns the same proxy when called twice with the same object.
<details><summary>Solution</summary>
Passing to / makes accessors run with , so their inner reads are logged too. The keeps proxy identity stable without keeping alive. It still fails for objects with private fields or internal slots (, ) — by design of .
</details>
Exercise 5 — A lazy pipeline
Using generators and iterator helpers, write that yields arrays of items, and use it to process the first 3 chunks of an infinite sequence without computing more.
<details><summary>Solution</summary>
calls on the chunk generator after the third chunk, which in turn exits its , which calls on . The infinite source stops cleanly.
</details>
Exercise 6 — Pick the copy
For each, choose spread, or neither: (a) update one field of a Redux state slice; (b) snapshot a form's state (with and values) for undo; (c) copy an instance of with methods; (d) send a nested config object to a worker.
<details><summary>Solution</summary>
(a) Spread — shallow is exactly right, and unchanged branches keep their identity for cheap equality checks. (b) — deep, keeps and . (c) Neither — both return plain objects; add to the class. (d) Neither by hand — already applies structured clone; just make sure the object holds no functions or class instances whose behaviour you rely on.
</details>
Cheat-sheet
| Topic | Rule ||---|---|| TDZ | // unusable until the declaration runs; does not help || Closure leak | Closures in one scope share one context; move long-lived callbacks to their own function || precedence | > // > > default ( in strict); arrows are lexical || Assignment vs definition | follows the chain (setters, read-only); and class fields do not || Brand check | (realm-safe) instead of || defaults | , , all || Integrity | < < ; all shallow || Iteration | → ; calls on early exit || Lazy pipelines | Generators + iterator helpers (, , , ), ES2025 || Proxy forwarding | || Proxy limits | Invariants on non-configurable props; no private fields or internal slots; slower || Finalizers | Not guaranteed to run; backstop only || Deep copy of data | (no functions, no prototypes) |
What's next
Post 03 is Async Engineering: promises as a state machine and their combinators, iteration and streams, cancellation with and , backpressure and concurrency limits, retries with jitter, / for deterministic cleanup (built on from this post), and error handling that survives production — unhandled rejections, error chains and aggregate errors. It builds on the queue model from Post 01 and the iterator protocols from this post.
References
- ECMAScript specification — Environment Records · Ordinary object internal methods · Proxy object internal methods and invariants- MDN —- MDN — Inheritance and the prototype chain- MDN — Public class fields · Private properties · Static initialization blocks- MDN —- MDN — Iteration protocols · Iterator helpers ()- MDN — ·- MDN — ·- MDN —- TypeScript —
JavaScript Deep Dive — 01 · Event Loop & Concurrency
"JavaScript is single-threaded" is true of your code on one thread and false of everything around it. The browser runs networking, parsing, compositing and garbage collection on other threads; Node runs file-system work on a thread pool; and both let you start real parallel threads of JavaScript with workers. What is single-threaded is each agent's event loop: one call stack, fed by queues of tasks and microtasks, in an order that is precisely specified — by the HTML standard in browsers and by libuv plus Node's own rules on the server. This post makes that order exact: tasks vs microtasks, where rendering and fit, the ordering puzzles that come out of it, Node's phases with and , and then true parallelism with workers, structured clone, transferables, and . It ends with the production view: long tasks, INP and yielding with .
> Series map: 00 Engines & Runtime → 01 Event Loop & Concurrency (this post) → 02 Objects, Prototypes & Metaprogramming → 03 Async Engineering → 04 Modules, Packaging & TypeScript at Scale → 05 Performance, Memory & Production Debugging. Future: see the extensions table in 05.
Learning Outcome
- Describe one turn of the browser event loop: task, microtask checkpoint, and the "update the rendering" steps.- Predict the output of any mix of synchronous code, , promises, and /, including extra promise ticks.- Place , and callbacks correctly in a frame.- Name Node's six event-loop phases and order , promise callbacks, and , in CommonJS and ES modules.- Choose between Web Workers, , and plain message passing, and know what structured clone can and cannot copy.- Use correctly and explain why cross-origin isolation is required in browsers.- Find and break up long tasks with and a fallback, to improve INP.
Chapter 1 — The model
Terminology
- Agent — a call stack, a set of queues and one event loop. A window (with its same-origin iframes), each worker, and each Node worker are separate agents.- Task (often "macrotask") — a unit of work queued by the host: a timer firing, an input event, a network response, a . Browsers keep several task queues (by task source) and may choose which queue to serve next — this is how input can be prioritised over timers.- Microtask — a smaller unit queued by promise reactions, , and . There is one microtask queue per agent.- Microtask checkpoint — the step that runs microtasks until the queue is empty, including microtasks queued by microtasks.- Rendering opportunity — a moment when the browser can present a new frame, typically aligned with the display refresh (about every 16.7 ms at 60 Hz). Browsers may skip frames or throttle hidden pages.
One turn of the browser loop
1. Pick the oldest task from a task queue the browser chooses, and run it to completion. The call stack empties.2. Perform a microtask checkpoint: run microtasks until none remain.3. If this is a rendering opportunity and there is something to render, update the rendering (Chapter 3).4. Repeat.
Two rules produce nearly every "surprising" order:
- Microtasks run whenever the JavaScript stack becomes empty, not only between tasks. The HTML spec's "clean up after running script" step performs a microtask checkpoint whenever the JavaScript execution context stack becomes empty — after a task, but also after each event listener the browser invokes and after each callback.- The checkpoint drains the queue completely. A microtask that queues another microtask keeps the loop in step 2. An infinite chain of microtasks freezes the page as surely as — rendering and input never get a turn.
> Watch out: is not "run next". HTML clamps the delay to at least 4 ms once timers are nested more than 5 levels deep, browsers throttle timers in background tabs heavily, and a timer is only a task — everything already queued, and the rendering step, can run first.
Chapter 2 — Ordering puzzles
Work each one out before reading the answer. All outputs below were checked in Node 22 (the same rules apply in browsers for these cases).
Puzzle 1 — the basics
Output: .
- , , are synchronous. The body of an function runs synchronously up to its first .- Three microtasks are queued in order: the callback (3), the callback (4), the continuation after (6).- The timer is a task, so comes after the microtask checkpoint.
Puzzle 2 — async functions calling async functions
Output: , , , , , , , .
The promise executor () runs synchronously. returns an already-fulfilled native promise; since ES2019, on a native promise takes a single microtask hop, so is queued before . (Older engines needed three hops and printed first — a well-known behaviour change, which is why old blog posts disagree.)
Puzzle 3 — returning a promise from
Output: .
Returning a thenable from a callback does not resolve the outer promise directly. The spec queues a , which calls , which queues another job when is already fulfilled. Two extra hops, so lands after .
| Microtask queue after each step | Queue contents ||---|---|| start | , || run (returns a promise) | , thenable job || run | thenable job, || run thenable job () | , resolve outer || run | resolve outer, || resolve outer (queues ) | , || run | , |
> Trick: in an function, inside is needed for the to see a rejection, but plain costs the same extra hops as Puzzle 3. The difference in hops almost never matters; the difference in error handling often does. Choose by semantics.
Puzzle 4 — user click vs
- Real user click: , , , . The browser calls each listener from the event loop, the stack empties after each, and a microtask checkpoint runs in between.- from script: , , , . The listeners run inside your script's call stack, which is not empty until returns and your script finishes.
This is why tests that dispatch synthetic events can pass while the real UI behaves differently — for example when a listener relies on a microtask (such as a framework's batched update) having run before the next listener.
Chapter 3 — Rendering and
When the loop decides to render, the HTML standard's update the rendering step runs, per document, in this order (abridged):
1. Run the resize steps (fire ), then the scroll steps (fire ).2. Evaluate media queries; update animations and send animation events; fullscreen steps.3. Run the animation frame callbacks — every callback registered before this frame began.4. Recalculate styles and update layout, then deliver callbacks; if they changed layout, recalculate and deliver again (to a depth limit, after which the browser reports a "ResizeObserver loop" error).5. Run the update steps.6. Paint — "update the rendering or user interface".
Consequences for engineers:
- rAF is the place for visual writes. Code in a rAF callback runs just before style and layout, so the frame shows its result. A animation drifts against the display's refresh and may run twice or zero times per frame.- A rAF callback that calls rAF schedules for the next frame, not the current one. That is what makes rAF loops work.- Microtasks run between rAF callbacks (the stack empties after each one), so a promise resolved in one rAF callback settles before the next callback runs.- Layout thrashing: reading layout (, ) after writing styles forces the browser to do style and layout synchronously, in the middle of your task. Doing it in a loop — write, read, write, read — can force a layout per iteration. Batch all reads first, then all writes.
> Common pitfall: using rAF in a background tab to keep "real-time" work going. Browsers stop or heavily throttle rAF in hidden documents (the HTML spec removes hidden documents from rendering). Anything that must keep running — a WebSocket heartbeat, a timer for session expiry — belongs in a timer or a worker, not rAF.
Chapter 4 — The Node.js event loop
Node's loop is libuv's loop plus two queues Node adds on top.
| Phase | Runs ||---|---|| timers | Expired / callbacks || pending callbacks | Some system I/O callbacks deferred from the previous iteration (e.g. certain TCP errors) || idle, prepare | Internal only || poll | Retrieves new I/O events and runs their callbacks; the loop blocks here when there is nothing else to do || check | callbacks || close callbacks | events such as |
The Node documentation notes one change: starting with libuv 1.45.0 (Node 20), timers are run after the poll phase in each iteration; earlier versions ran them before polling.
On top of the phases:
- queue — drained after the current operation completes, before the loop moves on, regardless of phase.- Microtask queue (promises, ) — drained right after the nextTick queue. Every time Node drains the nextTick queue, it drains the microtask queue immediately after.- Since Node 11, both queues are drained between every individual callback (after each timer, each immediate), matching browser behaviour. Older Node drained them only between phases.
vs
From the main module the order is not guaranteed — it depends on how quickly the loop starts relative to the 1 ms minimum timer. Inside an I/O callback it is deterministic: the loop is in the poll phase, so the check phase () always comes before the next timers phase.
vs promises: CommonJS vs ES modules
| File type | Output | Why ||---|---|---|| (CommonJS) | , , | The main script is an ordinary operation; the nextTick queue drains first, then microtasks || (ES module) | , , | ES module evaluation already runs as part of the microtask queue, so microtasks queued there run before Node gets back to the nextTick queue |
This is the example from the Node documentation, and it is the reason code that relied on ordering changes behaviour after a CommonJS-to-ESM migration.
> Trick: Node's own advice is to prefer over for deferring work in userland, and over for "after I/O". Reach for only when you need its specific guarantee — for example, emitting an event after the caller has had a chance to attach listeners, in a constructor.
> Watch out: a recursive (or a recursive microtask) starves I/O. The poll phase never runs, so sockets are not read and timers never fire. A recursive does not starve I/O: each round waits for the next check phase.
What runs off the main thread in Node
The event loop is single-threaded, but libuv has a thread pool (default 4 threads, ) for work without a good asynchronous OS API: most operations, , and CPU-heavy and calls. Network sockets do not use the pool; they use the OS's readiness APIs (epoll, kqueue, IOCP). A burst of calls or calls can therefore queue behind four busy threads — a classic hidden bottleneck.
Chapter 5 — Parallelism: workers
When work is CPU-bound — parsing a large file, image processing, compression, heavy validation — no scheduling trick helps; it must run on another thread.
| | Web Worker (browser) | (Node) ||---|---|---|| Create | | || Communicate | / , | / , , || DOM access | None | Not applicable || Shared memory | , only when cross-origin isolated | , always || Memory limits | Browser-managed | option (e.g. ) |
Each worker is a separate agent: its own isolate, its own heap, its own event loop. Starting one costs milliseconds and several megabytes, so create a pool once and reuse it rather than spawning a worker per request.
Structured clone and transferables
copies its argument with the structured clone algorithm (the same one behind ):
| Copied | Not copied (throws or is lost) ||---|---|| Primitives except symbols; | Functions, symbols → || Plain objects, arrays, , , , | DOM nodes → || , typed arrays, , , | Prototypes: a class instance arrives as a plain object || Error objects (, , …) | Private , getters/setters (only the current value is copied) || Circular references (preserved) | Property descriptors (everything becomes writable, enumerable) |
Copying a 100 MB buffer costs time and doubles memory. Transferring moves ownership instead: the sender's becomes detached ( 0) and the receiver gets the same memory with no copy. Transferable types include , , , , and the stream types , and .
> Trick: lets a worker own a canvas and render without touching the main thread: . Charting and map libraries use this to keep the main thread free for input.
Chapter 6 — Shared memory and
Message passing shares nothing, which is why it is safe. shares memory: both threads see the same bytes, with all the hazards of real threads.
Why browsers require cross-origin isolation
Shared memory plus a busy-loop counter gives a high-resolution timer, which is enough to mount Spectre-style side-channel attacks that read memory across origins. Browsers therefore expose only to cross-origin isolated documents, which requires two response headers on the top-level document:
Check at run time. blocks every cross-origin subresource that does not opt in with CORS or — third-party scripts, images and iframes break — so adopting it is a project, not a header change. Without isolation, of a throws. Node has no such restriction.
Data races and
operations are indivisible and sequentially consistent:
| Operation | Use ||---|---|| / | Read / write with ordering guarantees || , , , , , | Read-modify-write, returns the old value || | The building block for locks and lock-free structures || | Sleep while . Throws on a browser main thread; fine in workers and Node || | Non-blocking wait that returns (a promise when is true); usable on the main thread; Baseline since November 2025 || | Wake waiters |
> Watch out: gives you raw bytes, not objects. You cannot share a JavaScript object graph between threads; you design a binary layout (a ring buffer, a struct of typed-array fields) and serialise into it. That is the right tool for audio, video, physics or WASM threads — and the wrong tool for sharing application state, where message passing is simpler and safer.
Chapter 7 — Long tasks, INP and yielding
Terminology
- Long task — any task that occupies the main thread for more than 50 ms. Input that arrives during it waits.- INP (Interaction to Next Paint) — the Core Web Vital for responsiveness: roughly the worst latency from a user interaction to the next frame, across the page's life. Good is 200 ms or less; poor is above 500 ms.- Yielding — ending the current task deliberately so the browser can handle input and render, then continuing in a new task.
Yielding the right way
Splitting work with works, but the continuation goes to the back of the task queue, behind every other queued task (and third-party scripts). returns a promise whose continuation is queued with priority above other tasks of the same priority, so your work resumes promptly after the browser has handled input and rendering:
shipped in Chrome 129 and Firefox 142; at the time of writing it is not Baseline, so always feature-detect. is the companion API for scheduling new work with a priority. Microtasks are not a way to yield: resumes before the browser can render or handle input.
Finding the long tasks
- DevTools Performance panel: long tasks are flagged with a red corner; the flame chart shows which function owns the time.- Long Animation Frames API (LoAF): reports frames that took longer than 50 ms, with script attribution (source URL, function, invoker). It is the field-data tool for explaining a bad INP.- Node: gives a histogram of event-loop delay; a p99 in the hundreds of milliseconds means a synchronous hot spot (JSON parsing a huge body, a sync crypto call, a regex with catastrophic backtracking).
> Common pitfall: "we moved it into a promise, so it's async now". Wrapping CPU-bound work in or an function changes nothing — the executor and the code before the first run synchronously, and the rest runs in microtasks on the same thread. Only yielding (new tasks) or a worker (new thread) keeps the page responsive.
Practice Exercises
Exercise 1 — Order this
<details><summary>Solution</summary>
. is synchronous. The checkpoint runs , which queues timer C (after timer A) and microtask D; the checkpoint keeps draining, so runs before any task. Then the two timers in the order they were queued: , .
</details>
Exercise 2 — Starve the page
Write a four-line snippet that freezes a page forever using only promises, and explain why rendering never happens. Then change one call so the page stays responsive.
<details><summary>Solution</summary>
Each microtask queues another, and the checkpoint only ends when the queue is empty, so the loop never reaches the rendering step or the next task. Replace with (or ): each iteration becomes a separate task, and rendering and input can happen between them.
</details>
Exercise 3 — Node, inside an I/O callback
<details><summary>Solution</summary>
. After the I/O callback returns, Node drains the nextTick queue, then microtasks. The loop is in the poll phase, so the check phase () comes before the next timers phase.
</details>
Exercise 4 — The module-format bug
A library emits from its constructor with . The application attaches the listener one microtask later:
In the ESM application this logs . The same code in the CommonJS test suite never does. Explain, and fix the library.
<details><summary>Solution</summary>
In a file the top-level code already runs inside the microtask queue, so the promise callback (which attaches the listener) runs before Node returns to the nextTick queue, and the event is caught. In a file the nextTick queue drains first: is emitted before anyone listens, and it is lost (both outputs checked in Node 22). The code only ever worked by accident of queue order. Fix the library so late listeners cannot miss the state: expose it ( as a promise that stays resolved, or an flag the caller checks first), instead of a one-shot event fired on a timing guess.
</details>
Exercise 5 — Worker or not?
For each, choose main thread with yielding, a worker, or a worker with : (a) filtering a 5 000-row table as the user types; (b) parsing a 200 MB CSV upload; (c) a real-time audio effect exchanging samples with an ; (d) rendering 50 000 points to a canvas at 60 fps.
<details><summary>Solution</summary>
(a) Main thread; the work is small — debounce input and yield if it grows. (b) Worker: transfer the from in, stream results back in chunks. (c) + ring buffer: audio needs lock-free, copy-free exchange with hard deadlines (requires cross-origin isolation). (d) Worker with , so drawing never blocks input.
</details>
Exercise 6 — Break up a long task
A click handler runs over 20 000 items and takes about 400 ms. Rewrite it so input stays responsive and the first results appear quickly.
<details><summary>Solution</summary>
Each chunk stays under about 50 ms, so no long task blocks input for longer than that; the first yield lets the spinner render. For much larger lists, virtualise the list instead of rendering every item.
</details>
Cheat-sheet
| What | Rule ||---|---|| Browser turn | Task → drain microtasks → maybe render → next task || Microtask checkpoint | Whenever the JS stack empties; drains until empty || Microtask sources | //, , , || Task sources | Timers, input events, network, , || Render step order | resize/scroll → rAF → style & layout → ResizeObserver → IntersectionObserver → paint || clamp | ≥ 4 ms after 5 nested levels; throttled in background tabs || native promise | 1 microtask hop (since ES2019) || Return a promise from | 2 extra hops || Node phases | timers → pending → idle/prepare → poll → check → close || Node queues | nextTick queue, then microtasks, between every callback || CJS vs ESM | CJS: first; ESM: promises first || vs | Unordered from main module; immediate first inside I/O callbacks || libuv pool | 4 threads by default (): , , some / || Transfer, don't copy | , || SAB in browsers | + / ; check || Long task | > 50 ms; yield with + fallback |
What's next
Post 02 turns from when code runs to what it runs on: scopes, closures and the TDZ in precise terms, the four binding rules, the prototype chain and what really desugars to, private fields and static blocks, property descriptors, symbols and the iteration protocols, and with their invariants, and the weak collections that make caches safe. Post 03 then returns to async code — cancellation, backpressure, concurrency limits and error handling — building on this post's queue model.
References
- HTML Standard — Event loops — tasks, microtask checkpoint, update the rendering.- HTML Standard — Timers — the nesting-level clamp.- Jake Archibald — Tasks, microtasks, queues and schedules — the click vs example.- V8 blog — Faster async functions and promises — the hop change.- Node.js — The event loop, timers and- Node.js — vs- Node.js —- MDN — Structured clone algorithm · Transferable objects- MDN — security requirements ·- Chrome for Developers — Use to break up long tasks- web.dev — Interaction to Next Paint · Long Animation Frames API
AWS Course — 07 · Observability & Security Baseline
A workload that runs but can't be observed is unmaintainable; a workload without a security baseline is a liability. This post covers the production-ready ops layer every AWS account needs: CloudWatch (metrics, logs, alarms, dashboards), CloudTrail (API audit log), Systems Manager (Parameter Store, Patch Manager, Run Command), KMS (encryption keys), Secrets Manager (credentials), WAF (web firewall), and GuardDuty (threat detection). None of these were strictly required to deploy a workload in the previous posts — they're required to run one safely.
> Series map: 00 Foundations → 01 Network First → 02 Compute & Storage → 03 Scale & Survive → 04 Managed Data → 05 Serverless & Events → 06 Workflow Orchestration → 07 Observability & Security (this post) → 08 IaC.
Learning Outcome
- Read CloudWatch metrics, write logs, create alarms + dashboards.- Use CloudTrail to find "who did what when" in your account.- Store config in Parameter Store, secrets in Secrets Manager.- Use KMS to encrypt at rest with customer-managed keys.- Attach AWS WAF to ALB / API Gateway / CloudFront for L7 protection.- Enable GuardDuty + Security Hub for baseline threat detection.- Set up the minimum production observability checklist.
Chapter 1 — CloudWatch metrics
Every AWS service publishes metrics to CloudWatch automatically:
- EC2 — CPU, network in/out, status checks.- ALB — request count, target response time, 5XX count, healthy host count.- Lambda — invocations, errors, throttles, duration, concurrent executions.- RDS — CPU, free memory, IOPS, replica lag.- DynamoDB — consumed read/write capacity, throttles.
Metrics are stored per namespace (e.g., ) with dimensions (e.g., ). Each is a time series of values.
Custom metrics
For Lambdas, Embedded Metric Format (EMF) is cheaper — write a specific JSON line to logs, CloudWatch extracts it as a metric automatically.
Chapter 2 — CloudWatch Logs
Every Lambda, ECS task, EC2 (with CloudWatch Agent), API Gateway, RDS (optional), and ALB (optional) writes logs to log groups.
- Group: or .- Stream: one per execution / container / day.- Each log event has a timestamp + message.
Retention
The default retention is forever — costly over time. Set retention on every log group: 7, 14, 30, or 90 days are typical.
Querying
CloudWatch Logs Insights is a powerful query language:
> Trick: the most-overlooked CloudWatch cost is log ingest. A chatty Lambda producing 5 MB/min over a month = USD 1.30/GB × 200 GB = USD 260. Demote to for prod, sample noisy lines, and set short retention.
Chapter 3 — Alarms
A CloudWatch alarm watches a metric and notifies (usually via SNS) when a threshold is breached.
Minimum production alarms:
| Resource | Alarm ||----------|-------|| Every Lambda | || Every Lambda | || Every ALB | || Every ALB | || Every RDS | || Every RDS | || Every RDS | || Every DLQ | || Every NAT gateway | || Account | (in us-east-1) |
The SNS topic typically subscribes email + a Slack/Teams webhook + PagerDuty.
Chapter 4 — Dashboards
CloudWatch dashboards combine metrics + logs widgets into a single page. Reasonable starter set:
- Service KPI tile: request rate, p50 / p95 latency, 5xx count.- Resource utilisation row: CPU, memory, errors per service.- Cost tile: estimated charges + month-over-month.- Dependency row: DB CPU, queue depth, cache hit rate.
For multi-account visibility, CloudWatch cross-account observability lets one monitoring account aggregate metrics + logs from many workload accounts.
Alternatives
Datadog, Grafana / Prometheus, New Relic, Honeycomb — all integrate with CloudWatch. Pick whichever your team uses if it's already paid for. Pure-AWS dashboards are fine to start.
Chapter 5 — X-Ray (distributed tracing)
X-Ray traces a request as it hops between services (API Gateway → Lambda → DynamoDB → SNS). For each segment: start time, end time, latency, errors. Find bottlenecks in seconds.
Enable per service:- Lambda: tick "Active tracing" or .- API Gateway: enable in stage settings.- ECS: add the X-Ray daemon container.
In the SDK, instrument client calls:
X-Ray is being superseded by the newer AWS Distro for OpenTelemetry (ADOT) for cross-cloud + OSS compatibility. For pure AWS, X-Ray is still the simplest start.
Chapter 6 — CloudTrail (audit log)
CloudTrail records every API call in your account: who, when, what, from where.
By default, a free 90-day event history is available in the console. For longer retention and analytical use, configure a trail that writes to S3 + optionally CloudWatch Logs.
What CloudTrail tells you
- Who created / deleted resources.- Who assumed a role.- Failed authentication attempts.- Console logins (with MFA status).- Source IPs of API calls.
Couple with Athena to query CloudTrail logs in S3 cheaply.
Chapter 7 — Systems Manager (SSM)
SSM is a Swiss-army-knife. Four parts everyone uses:
| Part | What ||------|------|| Parameter Store | hierarchical key-value config; free for standard parameters || Secrets Manager (technically separate but adjacent) | rotating secrets, KMS-encrypted || Session Manager | shell on EC2 without SSH (Post 02) || Patch Manager | scheduled OS patching of EC2 / on-prem || Run Command | execute scripts across fleets || State Manager | enforce config (cron-like maintenance) || Inventory | what's installed where |
Parameter Store quick start
Apps fetch with the SDK at startup. Or use Lambda Extensions / EC2 user-data to pre-pull.
Chapter 8 — Secrets Manager
Specialised for credentials with automatic rotation. AWS rotates RDS / DocumentDB / Redshift passwords without app-code changes when wired correctly.
| Trait | Parameter Store SecureString | Secrets Manager ||-------|-------------------------------|------------------|| Cost | free | USD 0.40 / secret / month || Rotation | manual | built-in (RDS, custom Lambda) || Cross-account | manual | native sharing || Versioning | yes | yes |
For non-rotating config, Parameter Store SecureString is cheaper. For real credentials with rotation, use Secrets Manager.
> Trick: never store secrets in environment variables visible in the Lambda console / CloudFormation. Fetch from Secrets Manager at startup and cache in memory.
Chapter 9 — KMS (encryption keys)
KMS (Key Management Service) manages cryptographic keys for AWS services + your own code. Two key kinds:
| Kind | Use ||------|-----|| AWS-managed | each service has a default (, , etc.); free || Customer-managed (CMK) | you control rotation, key policy, deletion; USD 1 / month / key |
For most workloads, the default AWS-managed key is enough. Move to CMKs when:- Compliance demands it (HIPAA, FedRAMP).- You want to deny access to specific principals via key policy.- You want to enforce annual rotation.
Then use the key when creating an S3 bucket, RDS instance, EBS volume, etc., via the flag.
Chapter 10 — WAF and Shield
| Service | Protects | Cost ||---------|----------|------|| AWS WAF | ALB / API Gateway / CloudFront — application-layer (L7) | per-WebACL + per-rule + per-million requests || AWS Shield Standard | always-on DDoS protection for all AWS resources | free || AWS Shield Advanced | enhanced DDoS + 24/7 DRT support + cost protection | USD 3,000 / month / org |
Minimum WAF setup
A WebACL with these managed rule groups attached:- — OWASP top 10 staples.- — known exploit payloads.- — known bad IPs.- (paid) — bot detection.- A per-IP rate-based rule (e.g., block IP at 2000 req / 5 min).
Cost: USD 5 / WebACL + USD 1 / rule + USD 0.60 / million requests + USD 10 / rule for Bot Control. Affordable for most production sites.
Chapter 11 — GuardDuty and Security Hub
| Service | What ||---------|------|| GuardDuty | continuous threat detection (DNS exfil, crypto mining, unusual API patterns, leaked credentials) || Security Hub | aggregates findings from GuardDuty + Inspector + Macie + 3rd parties; scores against standards (CIS, PCI) || Config | tracks resource config + compliance against rules || Inspector | EC2 / Lambda / ECR vulnerability scanning |
A modern baseline enables GuardDuty + Security Hub + Config in every region of every account, ideally via an AWS Organizations delegated-admin pattern. Cost is modest relative to a single breach.
Chapter 12 — The production observability + security checklist
For each new workload:
Observability- [ ] Every Lambda / service writes structured logs with correlation IDs.- [ ] Every log group has retention ≤ 90 days.- [ ] Every service has dashboards for KPI + utilisation.- [ ] Every critical metric has an alarm with paging SNS subscribers.- [ ] X-Ray (or OpenTelemetry) is on across the request path.- [ ] Cost alarms exist at account level.
Security- [ ] Root user MFA enabled; root credentials in a vault.- [ ] CloudTrail multi-region trail to encrypted S3, log validation on.- [ ] GuardDuty enabled in all regions.- [ ] Security Hub aggregating findings.- [ ] No long-lived IAM access keys for code; everything uses roles.- [ ] Secrets in Secrets Manager, not in env vars or code.- [ ] S3 buckets: Block Public Access on, encryption on.- [ ] WAF in front of every internet-facing ALB / API Gateway.- [ ] Critical resources protected with deletion-protection / S3 Object Lock for backups.- [ ] Backup retention reviewed (RDS, EBS snapshots, DynamoDB PITR, S3 versioning).
Practice Exercises
Exercise 1 — Lambda error alarm
For the Lambda from Post 05, create an alarm on . Send notifications to an SNS topic. Subscribe your email. Force an error; confirm the alarm + email fire.
Exercise 2 — Log Insights query
Write a CloudWatch Logs Insights query that finds the 10 slowest invocations of a Lambda over the last hour.
Exercise 3 — Parameter Store + Secrets Manager
Move all the configuration (DB host, API keys, secrets) for one of your previous-post Lambdas out of environment variables: non-secret → Parameter Store, secrets → Secrets Manager. The function fetches at cold start, caches in module-level vars.
Exercise 4 — CloudTrail audit
In the CloudTrail event history console, find the API call that created your first VPC. Note the user, source IP, and timestamp.
Exercise 5 — WAF baseline
Attach a WebACL to the ALB from Post 03 with and a per-IP rate rule of 100 req / 5 min. Test with or that the rate rule actually blocks.
Exercise 6 — Enable GuardDuty
Enable GuardDuty in one region. Create a deliberate "suspicious" trigger (e.g., resolve a known-bad domain from an EC2). Wait for the finding to appear.
Cheat-sheet
| What | How ||------|-----|| Metric publish | or EMF JSON line || Log group retention | || Alarm | → SNS topic → email/Slack/PagerDuty || Dashboard | console widgets, or JSON via || Distributed tracing | X-Ray / OpenTelemetry || Audit log | CloudTrail multi-region trail to S3 || Config / non-secret | SSM Parameter Store || Secret with rotation | Secrets Manager || Customer-managed key | KMS CMK with annual rotation || L7 firewall | WAF managed rule groups + rate rule || DDoS basic | Shield Standard (free, always on) || Threat detection | GuardDuty + Security Hub || Vulnerability scan | Inspector || Resource config | Config rules + conformance packs || Log noise = cost | demote , sample, short retention |
What's next
Post 08 — the final base post — covers Infrastructure as Code: CloudFormation foundations, Terraform deep dive, and a survey of CDK / Pulumi / SAM so you can codify the entire architecture from Posts 01-07 into version-controlled, repeatable templates.
References
- CloudWatch User Guide- CloudTrail User Guide- Systems Manager User Guide- KMS Developer Guide- WAF Developer Guide- GuardDuty User Guide- AWS Whitepaper — AWS Well-Architected Security Pillar.
> Note: this post is in progress. Planned additions: CloudWatch synthetics, AWS Config conformance pack examples, IAM Access Analyzer walkthrough, OpenTelemetry collector deployment.
AWS Course — 06 · Workflow Orchestration — Step Functions and Patterns
Once you have many Lambdas + events (Post 05), the next problem is coordination: "run A, then B in parallel with C, then if A succeeded do D, retry B up to 3 times, and if anything fails roll back". Wiring this with raw Lambdas and SQS is possible but quickly turns into spaghetti. AWS Step Functions provides a managed state-machine service exactly for this purpose. This post covers the basics of Step Functions, the two workflow types (Standard vs Express), the common patterns (sequence, parallel, choice, map, wait, retry, catch), the Saga pattern for distributed transactions, and when to reach for Step Functions vs simpler alternatives.
> Series map: 00 Foundations → 01 Network First → 02 Compute & Storage → 03 Scale & Survive → 04 Managed Data → 05 Serverless & Events → 06 Workflow Orchestration (this post) → 07 Observability & Security → 08 IaC.
Learning Outcome
- Write a state machine in Amazon States Language (ASL).- Pick between Standard and Express workflows.- Use the core state types: , , , , , , , .- Configure retries, catches, and timeouts.- Implement a Saga (compensating-action rollback) for distributed transactions.- Decide between Step Functions, EventBridge Pipes, and SQS-chained Lambdas.
Chapter 1 — Why state machines
A workflow is a graph of steps with branches, parallelism, error handling, and (sometimes) human approval. Hand-rolled, this turns into a tangle of Lambdas invoking other Lambdas, with retry logic copied in each, error-handling baked into each step, and no central view of "where is this execution right now?".
Step Functions externalises the orchestration:
- The state machine definition is a JSON document — version-controlled, auditable.- Step Functions runs each execution: tracks position, retries, errors, history.- The console shows a visual diagram of every execution — which step succeeded, which failed, what data flowed where.- Each step is small and stateless (your Lambdas or AWS service calls); orchestration logic lives in one place.
Chapter 2 — Workflow types
| Type | Duration | Pricing | Execution history | Use ||------|----------|---------|-------------------|------|| Standard | up to 1 year | per state transition | full history, queryable | long-running, audit-required, occasional || Express | up to 5 min | per execution time | CloudWatch Logs only | high-throughput, short |
Standard suits business workflows (order fulfilment, ETL, approvals). Express suits per-request orchestration (an API call that needs to fan out, gather, return).
Chapter 3 — Amazon States Language (ASL)
A state machine is a JSON document with + :
Run:
Every state has a . The major ones follow.
Chapter 4 — Core state types
Task
Invokes an AWS service action. The most common is a Lambda, but Step Functions can call > 200 AWS service actions directly without a Lambda:
The suffix tells the engine "use the value from the execution state at this JSONPath". This service integration avoids a Lambda hop entirely — faster + cheaper + simpler.
Pass
Manipulates the state (filter, add, rename fields) without invoking anything.
Choice
Conditional branching.
Wait
Pauses for a duration or until a timestamp.
Parallel
Runs multiple branches at once; collects all results into an array.
Map
Like , but for a dynamic-size array — the same workflow runs once per item.
The 2023+ Distributed Map scales Map up to 10,000 parallel iterations (good for fanning out over big S3 lists, large arrays).
Succeed / Fail
Terminal states. records an error + cause that the parent can .
Chapter 5 — Retry and Catch
Every can declare retries and error catchers:
Retries multiply: first the most specific error block runs, then as a catch-all.
> Trick: put exponential backoff () on Retries that hit external APIs — the third-party will recover; instant retries waste your throttling budget.
Chapter 6 — The Saga pattern
In a distributed system, you can't have ACID across services. The Saga pattern fakes it: every step has a compensating action, and if a later step fails, the workflow runs the prior compensating actions in reverse.
Example: order placement.
If fails, the workflow:
1. Cancels delivery (n/a, never scheduled).2. Refunds the card.3. Releases the inventory.4. Returns the user a coherent failure response.
In Step Functions, model it as a block with that routes to a compensating sub-workflow.
Chapter 7 — Express vs Standard pricing
| Standard | Express ||----------|---------|| USD 0.025 per 1,000 state transitions | USD 1 per 1M executions + GB-second compute time |
A long-running workflow with 50 transitions costs USD 0.00125 per execution on Standard. A 200-ms Express execution at 100 MB costs USD 0.0000004 per execution. Express wins for high-volume; Standard wins for long-running / fewer / audit-required.
Many production systems use both: Standard for the business workflow, Express for inner high-frequency loops.
Chapter 8 — When NOT to use Step Functions
| Scenario | Better fit ||----------|------------|| Single Lambda, no branching | just use Lambda || Pure pub/sub fanout | SNS or EventBridge || Pure queue + worker | SQS + Lambda || Stream processing | Kinesis + Lambda || Simple event chain across AWS services without transformations | EventBridge Pipes (newer, lighter than Step Functions for source → enrich → target) || Always-on long-running daemon | ECS / EC2 |
EventBridge Pipes deserves a special mention: launched 2022, ideal for "read from queue/stream → optional Lambda enrich → write to target". For pure pipelines without branching, Pipes is cheaper and simpler than Step Functions.
Chapter 9 — A worked example: image-processing pipeline
Workflow: user uploads photo → resize multiple variants in parallel → run ML classifier → store metadata → notify user.
What this gains over hand-wired Lambdas:- Visual diagram for every execution.- Built-in retries with backoff.- Direct SDK integration for DynamoDB + SNS — no Lambda wrappers.- Full execution history queryable in the console.- Easy modification — add a step in JSON, no code change.
Chapter 10 — Standard workflow + human approval
A common pattern: wait for a human approval mid-workflow. Step Functions supports callback tokens — pause until an out-of-band call resumes.
The workflow pauses (up to a year on Standard). A human (or webhook) calls / with the token to resume.
Practice Exercises
Exercise 1 — Hello state machine
Create a Standard workflow with three Lambdas in sequence: A → B → C. Start an execution; watch the visual diagram in the console.
Exercise 2 — Choice branch
Add a Choice state after A:- if → Lambda X- else → Lambda Y
Test both inputs.
Exercise 3 — Retry + Catch
Make Lambda B fail intermittently (raise an exception 50% of the time). Add Retry (3 attempts, backoff 2) and Catch (→ a "Notify failure" Lambda). Observe behaviour.
Exercise 4 — Saga
Implement the order-placement Saga from Chapter 6 with three real Lambdas (each can be a stub that prints + returns). Force the third step to fail; verify compensating actions run in reverse.
Exercise 5 — Direct service integration
Replace one of your Task Lambdas with a direct DynamoDB . Confirm the workflow shrinks by one Lambda and runs faster.
Exercise 6 — Express vs Standard
Convert a workflow that runs 1000+ times/day to Express. Compare costs over a week.
Cheat-sheet
| What | How ||------|-----|| Workflow types | Standard (long, audit) vs Express (high-volume, short) || Definition | JSON (Amazon States Language) || Run | || Task state | invokes Lambda or AWS service directly || Choice | conditional branch || Parallel | run branches concurrently || Map | per-item iteration (Distributed Map for big arrays) || Wait | pause for time or until timestamp || Retry | per-Task; supports exponential backoff || Catch | route on error to recovery branch || Direct integrations | 200+ AWS service actions without Lambda || Callback token | pause for human approval / external system || Saga | every step has a compensating action || EventBridge Pipes | simpler source→enrich→target pipelines || Visual diagram | every execution; jump from console to details |
What's next
Post 07 introduces Observability & Security Baseline — CloudWatch, CloudTrail, Systems Manager, KMS, Secrets Manager, WAF. Now that the workload runs across compute, data, events, and workflows, you need to see what's happening and protect it.
References
- Step Functions Developer Guide- Amazon States Language spec- The AWS Workflow Studio — visual designer.- EventBridge Pipes overview- Serverless Land — Step Functions patterns
> Note: this post is in progress. Planned additions: Distributed Map example over a big S3 dataset; callback-token approval UI walkthrough; nested state machines; cost comparison Standard vs Express on a real workload.
JavaScript Deep Dive — 00 · Engines & Runtime
JavaScript is not "interpreted" and not "compiled" — in a modern engine it is both, several times over, and the engine changes its mind while your code runs. A function starts as bytecode, gets promoted to machine code when it turns hot, gets thrown back to bytecode when one of the guesses behind that machine code turns out wrong, and all the while a garbage collector moves its objects around in memory. Almost every "mysterious" performance cliff in a senior engineer's career — the loop that is fast on Monday and five times slower after a harmless refactor, the service whose p99 latency spikes every few seconds — comes from this machinery. This first post of the series opens up V8, the engine behind Chrome, Edge, Node.js and Deno: its compiler tiers, hidden classes, inline caches, deoptimization, elements kinds and the Orinoco garbage collector, then places the engine inside the runtimes that host it.
> Series map: 00 Engines & Runtime (this post) → 01 Event Loop & Concurrency → 02 Objects, Prototypes & Metaprogramming → 03 Async Engineering → 04 Modules, Packaging & TypeScript at Scale → 05 Performance, Memory & Production Debugging. Future: see the extensions table in 05.
Learning Outcome
- Separate the engine (ECMAScript) from the host (event loop, I/O, DOM, ) and say which one owns a given behaviour.- Trace a function through V8's pipeline: parser, Ignition bytecode, Sparkplug, Maglev, TurboFan, and back down on deoptimization.- Explain hidden classes (V8 "maps"), shape transitions and dictionary mode, and write object code that keeps shapes stable.- Classify an inline cache as monomorphic, polymorphic or megamorphic, and predict the cost of each.- Read output and name the common reasons a function deoptimizes.- Use elements kinds to explain why and a stray can slow a hot array loop.- Describe the Orinoco GC: scavenger, mark-compact, concurrent marking, write barriers — and read a line.- Compare browser engines and server runtimes (Node.js, Deno, Bun, Workers) at the architecture level.
Chapter 1 — Engine vs host
Terminology
- Engine — implements the ECMAScript specification: parsing, the object model, built-ins such as and , and the job queue for promise reactions. V8, SpiderMonkey and JavaScriptCore are engines.- Host (embedder) — the program that embeds the engine and gives it a world: the event loop, timers, networking, the DOM, the file system. Chrome's Blink, Node.js, Deno, Bun and Cloudflare's are hosts.- Isolate — one independent V8 heap with its own garbage collector. A browser tab's main thread, each Web Worker and each Node worker run in separate isolates. Isolates share no JavaScript objects.- Realm — one set of global objects and built-ins (, , ). An is a new realm in the same isolate, which is why fails for an array from another frame while does not.- Job — the specification's word for a queued unit of work. The engine asks the host to schedule promise jobs (); the host decides when they run. That is why microtask timing is defined by HTML and Node, not by ECMAScript alone.
The split matters in practice because the specification says nothing about , , , or the DOM. When behaviour differs between Chrome and Node, the engine is usually identical (both V8) and the host is the difference.
The landscape
| Environment | Engine | Host written in | Event loop | Notes ||---|---|---|---|---|| Chrome, Edge | V8 | C++ (Blink) | HTML event loop | Engine tiers covered in this post || Firefox | SpiderMonkey | C++ (Gecko) | HTML event loop | Tiers: interpreter, Baseline Interpreter, Baseline JIT, WarpMonkey (Warp) || Safari | JavaScriptCore | C++ (WebKit) | HTML event loop | Tiers: LLInt, Baseline, DFG, FTL || Node.js | V8 | C++ | libuv (phases, Post 01) | npm ecosystem, CommonJS + ESM || Deno | V8 | Rust | Tokio-based | Permissions model, web-standard APIs first || Bun | JavaScriptCore | Zig and C++ | Its own loop | Bundler, test runner and package manager in one binary || Cloudflare Workers | V8 isolates | C++ () | Per-request | Many isolates per process instead of one process per app |
Server runtimes converge on web APIs (, , , , , , ). That work is standardised by WinterTC (Ecma TC55), which took over from the W3C WinterCG community group in early 2025 and maintains a Minimum Common API for server runtimes.
> Trick: when a library says "works in Node, Deno, Bun and Workers", it usually means "uses only the Minimum Common API plus ECMAScript". That is the portable subset to target for shared code such as validation, date handling or API clients.
Chapter 2 — The V8 pipeline
V8 has one interpreter and three compilers. Each tier trades compile time for code quality:
| Tier | Kind | Shipped | Input → output | Why it exists ||---|---|---|---|---|| Ignition | Interpreter | V8 5.9 / Chrome 59 (with TurboFan) | Source → bytecode, then executes it | Fast start, small memory, collects type feedback || Sparkplug | Baseline (non-optimising) compiler | Chrome 91 | Bytecode → machine code in one linear pass, no IR | Removes interpreter dispatch cost almost for free || Maglev | Mid-tier optimising compiler | Chrome 117 | Bytecode + feedback → SSA/CFG IR → machine code | Good code quickly: about 10× slower to compile than Sparkplug, about 10× faster than TurboFan (V8 team's figure) || TurboFan | Top-tier optimising compiler | V8 5.9 | Bytecode + feedback → heavily optimised machine code | Peak speed for truly hot code; its JavaScript backend now runs on the CFG-based Turboshaft IR |
Parsing: most functions are parsed twice
Full parsing is expensive, and most functions in a large bundle never run. V8 therefore pre-parses inner functions: it checks syntax and records which variables they capture, but builds no AST and no bytecode. The full parse and bytecode generation happen on the first call ("lazy compilation"). A function that is called immediately pays twice, so V8 uses heuristics — for example a function expression wrapped in parentheses () is compiled eagerly.
Consequences:
- Startup cost is dominated by how much code is parsed, not how much exists. Code splitting and lazy help because unparsed code costs nothing.- Compiled bytecode can be cached. Chrome caches it for scripts it sees repeatedly; Node offers a module compile cache (, added in v22.1.0) that persists it to disk between runs, useful for CLIs.
Ignition: bytecode and feedback
Ignition is a register machine with a special accumulator register. You can print the bytecode of any function:
(Output trimmed and annotated; taken from Node 22 / V8 12.4. Opcode names change between V8 versions.)
The bracketed numbers after each operation are feedback slots. Every property access, call and arithmetic operation records what it saw — which object shapes, whether the operands were small integers or doubles — into the function's feedback vector. Everything the optimising tiers do is built on this record. Code that never ran has no feedback, and the optimisers cannot specialise it.
Tiering up and OSR
V8 counts how much a function runs (invocations and loop iterations, weighted by bytecode size) and promotes it when budgets run out. The exact thresholds are internal heuristics that change between releases; do not design around numbers.
A long-running loop inside a function that is called only once would never tier up by call count, so V8 performs on-stack replacement (OSR): it compiles an optimised version while the loop is running and jumps into it mid-loop.
> Watch out: Node's defaults do not always match Chrome's. Chrome has enabled Maglev since M117, but at least one Node 22 release line ships with as the default. Check your own binary with before assuming a tier exists.
Speculation is the whole trick
An optimising compiler for a dynamic language cannot know that is a small integer. It speculates from feedback: "every seen so far had shape M1, and was always a Smi". It emits a cheap check (a guard) followed by a fast path — a load at a fixed offset, an integer add. If the guard fails at run time, the optimised code cannot continue; it deoptimizes (Chapter 5). Fast JavaScript is JavaScript whose guards rarely fail.
Chapter 3 — Hidden classes (maps)
Terminology
- Map (V8's term; "hidden class" or shape elsewhere) — a descriptor attached to every heap object that records its property names, their order, their attributes and where each value is stored.- Transition — the edge from one map to another when a property is added. Maps form a transition tree rooted at the empty object's map.- In-object property — a value stored directly inside the object's memory; the fastest kind of access.- Properties backing store — an out-of-line array for properties that did not fit in-object.- Dictionary mode ("slow properties") — the object drops its map-based layout and stores properties in a hash table.
How shapes are built
Two objects share a map only if they received the same properties in the same order from the same starting point. You can check this with V8's internal functions (debugging only, never in production):
(Results from Node 22 / V8 12.4.)
For constructors and classes, V8 uses slack tracking: the first objects created get extra in-object space, and after a number of constructions V8 shrinks the allocation to what was actually used. Properties added long after construction end up in the out-of-line backing store — still fast, one extra pointer hop.
Rules that keep shapes stable
1. Initialise every field in the constructor (or as a class field), in the same order, even if the value is . An object that gains later in some code paths splits the transition tree.2. Do not properties on hot objects. Set them to or . usually sends the object to dictionary mode for good (shown above).3. Use for dictionaries. An object used as a hash table with arbitrary keys will go to dictionary mode anyway, and then and are slower than iteration.4. Keep numeric field types consistent. A field that holds Smis in some objects and doubles in others forces the map to generalise the field's representation.5. Do not mutate prototypes of live objects (, assigning ). Prototype maps are special; changing them invalidates optimised code that depended on them.
> Common pitfall: "optional" fields added by spreading different objects: creates two shapes from one call site. In a hot path, write so every result has the same keys in the same order.
Chapter 4 — Inline caches and polymorphism
An inline cache (IC) is the feedback slot of one property access or call site. It remembers which maps it has seen and, for each, where the property lives.
| IC state | Maps seen | What the fast path does | Optimisable? ||---|---|---|---|| Uninitialised | 0 | Nothing yet; the access has not run | No feedback, the optimiser inserts a deopt point || Monomorphic | 1 | One map check, then a load at a known offset | Best case || Polymorphic | 2–4 | A short chain of map checks | Yes, slightly slower per extra map || Megamorphic | more than 4 | Gives up per-site caching; uses a global stub cache (hash table) | TurboFan cannot inline the access |
The cut-off is 4 maps in V8. A one-off measurement on Node 22 (V8 12.4, x86-64 cloud VM) with the script below shows the step clearly — about 26 ms with 1 shape, 47 ms with 4, and 114 ms with 5. Treat these as one run on one machine, not a benchmark result; the shape of the curve is what matters.
> Watch out: feedback belongs to the function, not to your benchmark case. Running several cases one after another through the same in one process gives nonsense, because the IC accumulates every shape it has ever seen. Isolate cases in separate processes.
Where megamorphism comes from in real code
- Generic utilities — a helper, a deep-equal, a serializer, a logger's — see every shape in the program. They are megamorphic by design. That is acceptable if they are not your hot loop.- Component frameworks — a renderer that reads on thousands of different component prop objects. Framework authors work hard to keep internal objects (fibers, vnodes) one shape, and you should keep your own hot records the same way.- Heterogeneous arrays — an array of events where each type is a differently shaped object. Consider a single shape with a tag and the same fields (some ) for the hot processing path.- Class hierarchies — instances of different subclasses have different maps. A loop over calling is polymorphic in the call site; with more than four subclasses it goes megamorphic.
> Trick: normalise at the boundary. Parse external data (JSON, database rows) into objects created by one factory or class with a fixed field order. The rest of the program then sees one map.
Chapter 5 — Deoptimization
When a guard in optimised code fails, V8 reconstructs the interpreter frame from the optimised frame (the compiler keeps metadata for this) and continues in Ignition or Sparkplug. The function can be optimised again later with the new feedback.
| Kind | Trigger | Example ||---|---|---|| Eager | A guard fails inside optimised code | was optimised for Smis, then receives a string || Lazy | An assumption is invalidated elsewhere; the code is marked and deopts when control returns to it | Someone modifies a prototype or a constant field that the code depended on || Soft | The optimiser reached code that had no feedback yet | A branch that never ran before optimisation |
Watching it happen:
The two fields to read are kind and reason. Common reasons: (shape changed), / (number representation changed), (array index past length), , .
Failure modes
- Deopt loops. A function optimises, deopts, re-optimises with slightly wider feedback, deopts again. Each round throws away compile work, and the function spends much of its life in the lower tiers. The symptom is a function that stays slow however hot it is. shows the cycle.- One-time deopts are normal. A few at startup cost microseconds. Chase deopts only when a profile shows the function is hot and the trace shows repetition.- Stale advice. " disables optimisation" and "never use " date from Crankshaft, the compiler TurboFan replaced in 2017. TurboFan optimises . Rest parameters are still clearer than , but for readability, not speed.
Measuring without fooling yourself
Microbenchmarks in a JIT are hazardous:
- Warm-up. The first iterations run in Ignition. Measure after warm-up, or measure a realistic workload end to end.- Dead-code elimination. If the result is unused, TurboFan may delete the work. Always consume the result (the above).- Constant folding. Inputs known at compile time can be folded away. Take inputs from or data.- Feedback pollution. Covered above: separate processes per case.- GC noise. An allocation-heavy loop measures the garbage collector as much as your code. Run several times and report the spread.
> Trick: use natives only to explain a result you have already measured, never in shipped code. They are internal, unstable and disabled without .
Chapter 6 — Numbers and elements kinds
Smis and heap numbers
V8 represents small integers as Smis — tagged values stored directly in the pointer, with no allocation. With pointer compression (Chrome on 64-bit) a Smi is 31 bits (−2³⁰ to 2³⁰−1); builds without pointer compression, typical for Node, use 32-bit Smis on 64-bit platforms. Anything else — fractions, , , , integers outside the Smi range — is a HeapNumber (a boxed double), unless the optimiser can keep it unboxed in a register or a double field.
Elements kinds
Array-indexed properties (the elements) are tracked separately from named properties, with their own "kind". V8 distinguishes 21 elements kinds; the six you meet daily form a lattice:
Transitions only go right or down, towards more general kinds — never back:
Why holes cost: reading a hole means "not here — look up the prototype chain", so every holey read needs an extra check, and the engine must be sure nobody added indexed properties to or .
Practical rules (from the V8 team's own guidance):
- Prefer literals and over , which creates a holey array. If you need a pre-sized numeric buffer, use a typed array (), which has no holes and a fixed element type.- Keep , and out of integer arrays unless they are real data; one such value turns a Smi array into a double array.- Do not read past ( style loops); an out-of-bounds read walks the prototype chain and can deopt.- Convert array-likes (, ) to real arrays once, then loop over the array.
> Common pitfall: is still HOLEYSMIELEMENTS — puts values in the holes but the array stays holey (holey is permanent). builds a packed array. The difference only matters in genuinely hot numeric code; do not rewrite ordinary code for it.
Chapter 7 — The Orinoco garbage collector
Terminology
- Generational hypothesis — most objects die young. A GC that collects young objects often and cheaply wins.- Young generation — where new objects are allocated. In V8 it is split into a nursery and an intermediate generation.- Old generation — objects that survived two young collections.- Scavenge (minor GC) — copying collection of the young generation.- Mark-Compact (major GC) — marking live objects in the whole heap, sweeping dead ones, compacting fragmented pages.- Write barrier — a small piece of code the engine runs on every pointer store into the heap, so concurrent or incremental marking does not miss new references, and old-to-young pointers are remembered.- Stop-the-world pause — time the main thread spends in GC instead of running JavaScript.
Young generation: the scavenger
New objects go into the nursery with bump-pointer allocation (very cheap). When the young space fills, a scavenge copies live objects out: survivors of their first scavenge become intermediate; survivors of a second are promoted to the old generation. The young generation uses a semi-space design, so half of it is always empty to receive the copies. Cost is proportional to the number of live objects, not the garbage — dying young is nearly free.
The scavenger runs in parallel on helper threads; the V8 team measured a 20–50% reduction in main-thread young-generation GC time from parallel scavenging.
Node exposes the size knob: (MiB). The Node docs note that the young generation is three times the semi-space size, so +1 MiB of semi-space adds 3 MiB of heap. Raising it can cut scavenges for allocation-heavy servers; benchmark before and after.
Old generation: concurrent mark-compact
- Marking finds everything reachable from the roots (stack, globals, handles). V8 does most of it concurrently on background threads while JavaScript runs, plus incremental steps on the main thread. Write barriers keep it correct while your code mutates the heap.- Sweeping frees dead objects' memory into free lists, concurrently.- Compaction moves objects out of fragmented pages, in parallel, during a pause.- Large objects (above a size threshold) live in a separate large-object space and are never moved.- Idle-time GC: the browser tells V8 when a frame finished early, and V8 uses the idle slice for GC work.
Concurrent marking and sweeping reduced pause times in heavy WebGL games by up to 50%, per the V8 team.
In Chrome, Blink's C++ DOM objects are managed by Oilpan () and traced together with the JavaScript heap, so a cycle between a DOM node and a JavaScript closure is still collectable.
Reading
| Field | Meaning ||---|---|| / | Minor or major GC || | Used heap (committed heap) before → after || | Main-thread pause / time spent in external (embedder) callbacks || | Mutator utilisation: the fraction of time spent running JavaScript rather than GC. 1.000 is perfect || trailing text | Why it ran: (young space full) is the normal reason |
Healthy services show frequent sub-millisecond scavenges and rare mark-compacts. Warning signs: mark-compacts that free little memory ( after a 224 ms pause is a heap full of live data), trending down, and heap "after" values that climb from one major GC to the next — a leak, which Post 05 hunts with heap snapshots.
> Watch out: is a ceiling, not a target. Setting it above the container's memory limit lets the heap grow until the kernel OOM-kills the process instead of V8 throwing a clean "heap out of memory". Set it below the container limit, leaving room for buffers, native memory and the young generation.
Practice Exercises
Exercise 1 — Engine or host?
For each, say whether the engine or the host defines it: (a) , (b) , (c) the order of promise callbacks relative to each other, (d) when promise callbacks run relative to a event, (e) , (f) resolving a bare specifier like .
<details><summary>Solution</summary>
(a) Engine — ECMAScript built-in. (b) Host — HTML / Node timers. (c) Engine — the spec requires promise jobs to run in the order they were enqueued. (d) Host — HTML decides when it performs a microtask checkpoint. (e) Host — defined by the HTML standard, adopted by server runtimes. (f) Both: syntax and module semantics are ECMAScript, but resolving a specifier to a file or URL is host-defined (Node's resolution algorithm, a browser import map).
</details>
Exercise 2 — Predict the maps
How many distinct maps end up in ?
<details><summary>Solution</summary>
List the property sequences: i=0 , i=1 , i=2 , i=3 , i=4 , i=5 . Distinct: , , , — four maps. A function reading across this list is polymorphic (4 maps, the limit). Add one more optional field and it goes megamorphic. Fix: gives one map.
</details>
Exercise 3 — Reproduce the megamorphic step
Run from Chapter 4 for n = 1, 2, 3, 4, 5, 6 on your machine. Where is the jump? Then change to read a property added after the varying one ( after the loop line, read ). Does anything change?
<details><summary>Solution</summary>
The jump appears between 4 and 5 shapes, where the IC turns megamorphic. Reading instead of still sees n maps (the maps differ because of the earlier varying property), so the curve is the same. Map identity depends on the whole transition path, not just the property being read.
</details>
Exercise 4 — Read a deopt
A trace shows repeating every few seconds for . What changed in the data, and what are two fixes?
<details><summary>Solution</summary>
was optimised on feedback that a value was always a small integer; now it periodically receives a fraction or a large integer (e.g. a price like , or an ID above the Smi range). Fixes: (1) make the representation consistent from the start — e.g. keep money in integer cents, or ensure warm-up covers doubles so the optimiser plans for them; (2) stop mixing types — parse values to numbers once at the boundary instead of sometimes receiving strings. Repeating deopts point to a deopt loop; confirm with .
</details>
Exercise 5 — Elements kinds
Give the elements kind after each line: · · ·
<details><summary>Solution</summary>
→ HOLEYSMIELEMENTS. Filling all three slots → still HOLEYSMIELEMENTS (holey is permanent). → PACKEDDOUBLEELEMENTS ( builds a fresh packed array; 0.5 and 1.5 are doubles). → PACKEDDOUBLEELEMENTS, because cannot be a Smi.
</details>
Exercise 6 — Diagnose a GC trace
A Node API in a 1 GiB container runs with . Under load, shows Mark-Compact lines every few seconds, each pausing 300–600 ms and freeing under 5% of the heap; then the pod restarts with exit code 137. Explain and fix.
<details><summary>Solution</summary>
The heap is mostly live data (a leak or an unbounded cache), so each major GC works hard and frees little, and pauses grow with heap size. Because the V8 limit (4096 MiB) is above the container limit (1 GiB), V8 never throws "heap out of memory"; the kernel's OOM killer ends the process (137 = SIGKILL). Fix: set below the container limit (for example around 700–768 MiB for 1 GiB, leaving room for native memory) so failures are visible and diagnosable, then find the retained objects with heap snapshots (Post 05) — usually an unbounded , a listener that is never removed, or closures captured in a global.
</details>
Cheat-sheet
| What | How ||---|---|| V8 tiers | Ignition (bytecode) → Sparkplug (baseline) → Maglev (mid) → TurboFan (top) || Print bytecode | || Watch optimisation | || Check a flag's default | || Same shape? (debug only) | , then || Stable shapes | All fields in constructor, same order; no ; for dictionaries || IC states | 1 map mono · 2–4 poly · more than 4 megamorphic || Deopt kinds | Eager (guard fails) · Lazy (assumption broken elsewhere) · Soft (no feedback) || Fast arrays | Literals / ; avoid holes, , ; typed arrays for numeric buffers || GC trace | || Young-gen size | (young gen = 3 × semi-space) || Old-gen ceiling | , below the container limit || Startup cache | (Node 22.1+) || Portable server API | WinterTC Minimum Common API |
What's next
Post 01 moves from the engine to the host: the event loop. The call stack, tasks and microtasks, where rendering and fit in a browser frame, output-ordering puzzles that trip up senior interviews, Node's libuv phases with and , and real parallelism with Web Workers, , and .
References
- V8 blog — Launching Ignition and TurboFan — the 2017 pipeline change.- V8 blog — Sparkplug, a non-optimizing JavaScript compiler- V8 blog — Maglev, V8's fastest optimizing JIT- V8 blog — Land ahoy: leaving the Sea of Nodes — Turboshaft.- V8 blog — Fast properties in V8 — in-object, fast and dictionary properties.- V8 blog — Elements kinds in V8- V8 blog — Trash talk: the Orinoco garbage collector- Mathias Bynens & Benedikt Meurer — JavaScript engine fundamentals: Shapes and Inline Caches- Node.js CLI options — , , .- ECMAScript specification — Jobs and host operations- WinterTC (Ecma TC55) — the Minimum Common API for server runtimes.
AWS Course — 05 · Serverless & Events — Lambda, API Gateway, SQS, SNS, EventBridge
Post 03 scaled an EC2 fleet behind a load balancer; Post 04 added managed data behind it. Most modern AWS architectures are not all-server, all-the-time — large parts of them run as events flowing through serverless glue. This post covers the building blocks: Lambda (run code on demand), API Gateway / Function URLs (turn Lambda into HTTP), SQS (queues for decoupling), SNS (pub/sub fanout), and EventBridge (the bus for routed events). Combined, they let you build entire services with no servers to manage and bills proportional to traffic.
> Series map: 00 Foundations → 01 Network First → 02 Compute & Storage → 03 Scale & Survive → 04 Managed Data → 05 Serverless & Events (this post) → 06 Workflow Orchestration → 07 Observability & Security → 08 IaC.
Learning Outcome
- Write and deploy a Lambda function (Python or Node).- Configure Lambda memory, timeout, environment, IAM role, layers.- Expose Lambda over HTTP via API Gateway or Function URLs.- Use SQS for async work decoupling and back-pressure.- Use SNS for fanout to multiple subscribers.- Use EventBridge for cross-service event routing and SaaS integrations.- Identify cold-start sources and mitigation patterns.
Chapter 1 — Lambda essentials
A Lambda function is a piece of code (handler) plus configuration (memory, timeout, role, env vars) that AWS runs in response to an event. Billing is per-request plus per-GB-second of execution time.
Minimal Python Lambda
Package and deploy:
Knobs that matter
| Knob | Range | Note ||------|-------|------|| Memory | 128 MB - 10,240 MB | CPU scales with memory — doubling memory ≈ doubles speed || Timeout | 1 sec - 15 min | killed if exceeded || Ephemeral storage | 512 MB - 10 GB at | persists across warm invocations || Concurrency | up to 1000+ by default | reserved / provisioned controls || VPC | none (default) or attached | only attach if you need VPC resources |
Trick: memory is the most important tuning knob. Tools like Lambda Power Tuning test multiple memory sizes and recommend the sweet spot — usually faster AND cheaper than the 128 MB default.
Chapter 2 — Cold starts
When AWS provisions a fresh container for a function, the cold start cost is:1. Download the zip / image.2. Initialise the runtime.3. Run module-level / static code.4. Invoke the handler.
After the first invoke, the container stays warm a few minutes — subsequent calls skip steps 1-3.
| Runtime | Typical cold start ||---------|--------------------|| Python / Node | 100-500 ms || Go / Rust (compiled) | 50-200 ms || Java | 300 ms - 2 s || .NET | 200 ms - 1 s || Container image | 0.5 - 3 s |
Mitigation
- Provisioned Concurrency — keep N warm containers ready; always-on cost.- SnapStart (Java) — snapshot init state; restore in 1/10 the time.- Smaller package — fewer dependencies, faster init.- Avoid VPC unless needed — ENI attach adds 100-500 ms.- Move heavy setup to module level — reused across warm invocations.
Chapter 3 — Event sources
Lambdas don't run on a schedule by default — they're invoked by event sources.
| Source | Pattern ||--------|---------|| API Gateway / Function URL / ALB | sync HTTP → Lambda → HTTP response || S3 | new object created / deleted → Lambda || EventBridge schedule | cron / rate → Lambda || EventBridge events | AWS API event → Lambda || SQS | poll a queue → Lambda || SNS | publish → Lambda || DynamoDB Streams | row change → Lambda || Kinesis | stream record → Lambda || CloudWatch Logs subscription | log line → Lambda || Direct invoke (SDK / CLI) | sync or async call |
Sync vs async vs polling
- Sync — caller waits for response (API Gateway, Function URL, direct invoke).- Async — Lambda returns immediately, processes in background; failures retried twice + optional DLQ (S3, SNS, EventBridge).- Polling — Lambda runtime polls SQS / Kinesis / DynamoDB Streams in batches.
Chapter 4 — API Gateway and Function URLs
Two ways to put HTTP in front of Lambda:
API Gateway HTTP API
- Custom domain + TLS via ACM.- Authn (JWT, Cognito, IAM, Lambda authoriser).- Throttling, usage plans, API keys.- Per-route Lambda integration or HTTP/private integration.- USD 1 per million requests + data transfer.
Minimal wire-up: create HTTP API → add route → Lambda integration → deploy → get an URL.
Lambda Function URL
- One HTTPS endpoint per Lambda.- No API Gateway in between (cheaper, simpler).- No built-in WAF, custom domain (manual via CloudFront), or throttling.- Best for internal tools, webhooks, prototypes.
REST API (older)
The older "REST API" flavour exists for compatibility (request validation, models, X-Ray detail). For new projects in 2026, HTTP API is the right default.
Chapter 5 — SQS (Simple Queue Service)
SQS is a managed message queue. Producers , consumers (often Lambda) + after success.
Two queue types:
| Type | Order | Duplicates | Use ||------|-------|------------|-----|| Standard | best-effort | possible | most async work || FIFO | strict per group | exactly-once | order-sensitive workflows |
Lambda + SQS pattern
Benefits:- Decoupling — A returns immediately; processing runs in background.- Back-pressure — if Lambda concurrency caps, messages sit in queue (no data loss).- Retries — Lambda errors keep message visible; goes to DLQ after N attempts.- Batch processing — Lambda receives up to 10 messages at once.
Visibility timeout
When a consumer receives a message, it becomes invisible to others for the visibility timeout (default 30 sec). If not deleted by then, the message reappears for another consumer. Set this to at least 2× your processing time.
Dead Letter Queue (DLQ)
Configure a DLQ on a source queue. After N receive attempts (typical: 3-5), the message moves to the DLQ for inspection. Alarm on DLQ message count > 0 — that means production has a stuck pattern.
Chapter 6 — SNS (Simple Notification Service)
SNS is pub/sub fanout — a publisher sends one message to a topic; AWS delivers a copy to every subscriber.
Subscriber types:- SQS queue- Lambda- HTTPS endpoint- Email / SMS- Mobile push- Kinesis Firehose
Common fanout pattern
Each subscriber gets its own copy. Adding a new subscriber doesn't require changing the publisher.
Filter policies
Subscribers can filter by message attributes — only receive a subset:
Chapter 7 — EventBridge (the bus)
EventBridge is a richer event router than SNS:
| Trait | SNS | EventBridge ||-------|-----|-------------|| Fanout | yes | yes || Filter | by attributes | by content (JSON match) || Schedule | no | yes (cron / rate) || SaaS integrations | no | many (Datadog, Zendesk, GitHub via partner sources) || AWS event sources | no | every AWS service auto-publishes || Schema registry | no | yes || Latency | < 100 ms | < 500 ms || Cost | per published message | per matched rule |
Bus types
- Default bus — every AWS API event lands here automatically. React to "EC2 state change", "S3 object created", "RDS failover", etc.- Custom bus — your own events.- Partner bus — events from SaaS providers (Datadog, Auth0, Zendesk).
Targets
A rule routes matching events to targets:- Lambda- Step Functions- SQS- SNS- Kinesis- API destinations (any HTTPS endpoint)- Cross-account / cross-region buses
Scheduled rules
The cleanest way to run "every 5 minutes" or "every weekday 09:00 HKT":
In 2026, the newer EventBridge Scheduler service is preferred for pure schedules — better scale, more options (time zones, one-time, payload customisation).
> Trick: SNS for simple fanout (one source → many same-message subscribers). EventBridge for routing different events to different places, especially when AWS services are the source or you want content-based filtering. Both are cheap; pick by access pattern, not cost.
Chapter 8 — A small end-to-end serverless service
Pattern: "user uploads image → resize → notify uploader".
Components:- S3 bucket with event notification → resize Lambda.- Resize Lambda uses Pillow (Python image lib) — package as a Lambda Layer.- SNS topic with two subscribers.- DynamoDB table for analytics.- All wired with IAM roles scoped to least-privilege.
This entire architecture has zero servers to manage and bills only when an image is uploaded.
Chapter 9 — Limits worth remembering
| Limit | Value ||-------|-------|| Lambda timeout | 15 min || Lambda memory | 10 GB || Lambda zip package | 50 MB direct, 250 MB unzipped || Lambda container image | 10 GB || Lambda concurrency (default) | 1000 / region / account || Lambda sync payload | 6 MB || Lambda async payload | 256 KB || Lambda | 10 GB || SQS message size | 256 KB (link to S3 for bigger) || SQS retention | 14 days max || SNS message size | 256 KB || EventBridge event size | 256 KB || API Gateway timeout | 29 sec (sync) |
Chapter 10 — When NOT to use serverless
- Long-running CPU jobs (> 15 min) — use Step Functions (Post 06), ECS Fargate, or AWS Batch.- Steady high-throughput services where always-on EC2 / Fargate is cheaper than per-request Lambda billing.- Cold-start-sensitive workloads where even 100 ms hurts and Provisioned Concurrency cost wipes the savings.- Heavy WebSocket / persistent connections — use API Gateway WebSocket + Lambda or ECS.
Serverless wins when traffic is spiky, glue-shaped, or genuinely event-driven. Steady-state high-CPU workloads belong on Fargate / EC2.
Practice Exercises
Exercise 1 — Hello Lambda
Create + invoke a Python Lambda that echoes the event. Test with the CLI.
Exercise 2 — HTTP API + Lambda
Wrap Exercise 1 in API Gateway HTTP API. Hit the URL with . Add a route that returns the JSON body.
Exercise 3 — S3 thumbnail trigger
Create an S3 bucket; a Lambda triggered on that, given an image, writes a half-size thumbnail to a sibling bucket. Use a Lambda Layer for Pillow.
Exercise 4 — SQS + Lambda batch
Create an SQS queue + a Lambda triggered by it. A simple producer script enqueues 100 jobs. Verify Lambda processes them in batches, set the DLQ, force a few failures and confirm they land in the DLQ after retries.
Exercise 5 — SNS fanout
Create an SNS topic; subscribe a Lambda + an SQS queue. Publish a message. Confirm both receive the message. Add a filter policy so the queue receives only messages with .
Exercise 6 — EventBridge schedule
Create an EventBridge schedule that triggers a Lambda every 5 minutes. The Lambda fetches and writes the stargazer count to DynamoDB.
Cheat-sheet
| What | How ||------|-----|| Create Lambda | || Invoke | || Tail logs | || Memory tunes CPU | bigger = faster + (often) cheaper || Skip VPC unless needed | avoid cold-start ENI attach || HTTP wrapper | API Gateway HTTP API || Tiny HTTP endpoint | Lambda Function URL || Async decoupling | SQS Standard + Lambda || Order-sensitive | SQS FIFO || Visibility timeout | ≥ 2× processing time || DLQ | alarm on DLQ count > 0 || Pub/sub fanout | SNS topic + subscribers || Filter subscriptions | SNS filter policy || Cross-service routing | EventBridge bus + rules || AWS-event source | EventBridge default bus || Scheduled job | EventBridge Scheduler |
What's next
Post 06 covers Step Functions — the state-machine service that orchestrates multiple Lambdas / API calls / human steps into a workflow with retries, branching, parallelism, and the saga pattern for distributed transactions.
References
- Lambda Developer Guide- API Gateway docs- SQS docs- SNS docs- EventBridge docs- Serverless Land — reference patterns, samples.
> Note: this post is in progress. Planned additions: WebSocket APIs, EventBridge Pipes, Kinesis vs SQS decision tree, container-image Lambdas.
Abstract Algebra — 05 · Field Extensions and Galois Theory
Post 04 ended by building new fields: $\mathbb{C} = \mathbb{R}[x]/(x^2+1)$ and $\mathbb{F}4 = \mathbb{F}2[x]/(x^2+x+1)$. This post studies such constructions systematically. A bigger field $E$ containing $F$ is a vector space over $F$, and its dimension, the degree, turns out to be a very strong invariant: a single divisibility argument proves that no ruler-and-compass construction doubles a cube or trisects a 60° angle. Galois theory then attaches to a polynomial a finite group, the permutations of its roots that respect all algebraic relations, and the Fundamental Theorem matches the subfields of the splitting field with the subgroups of that group. Everything from posts 01–04 comes together here: groups, normal subgroups and quotients, group actions, Sylow-type counting, irreducible polynomials and quotient rings. The chapter ends with the reason there is no quintic formula.
> Series map: 00 Integers, Relations & Modular Arithmetic → 01 Groups & Subgroups → 02 Cosets, Lagrange & Quotient Groups → 03 Homomorphisms, Group Actions & Sylow → 04 Rings, Ideals & Fields → 05 Field Extensions & Galois Theory (this post).
Learning Outcome
- Compute the degree of a field extension, and prove and use the tower law.- Find minimal polynomials, and prove that $F(\alpha) \cong F[x]/(m\alpha)$ with degree $\deg m\alpha$.- Show that $\mathbb{Q}(\sqrt2, \sqrt3)$ has degree 4 and is generated by $\sqrt2 + \sqrt3$.- Prove that doubling the cube and trisecting a 60° angle are impossible with ruler and compass.- Build splitting fields, and prove that there is one finite field of each prime-power order, up to isomorphism.- Define separable, normal and Galois extensions and the Galois group.- State the Fundamental Theorem of Galois Theory and draw the subfield/subgroup lattices for $\mathbb{Q}(\sqrt2, \sqrt3)$ and for the splitting field of $x^3 - 2$.- Define solvable groups and outline why the roots of $x^5 - 4x + 2$ cannot be written with radicals.
Chapter 1 — Field extensions and degree
Terminology
- Extension — a pair of fields $F \subseteq E$, written $E/F$ ("$E$ over $F$"). $F$ is the base field.- Degree — $[E : F] = \dimF E$, the dimension of $E$ as a vector space over $F$. The extension is finite if the degree is finite.- Generated extension — for $\alpha \in E$, $F(\alpha)$ is the smallest subfield of $E$ containing $F$ and $\alpha$. Likewise $F(\alpha1, \dots, \alphan)$. An extension $F(\alpha)/F$ is simple and $\alpha$ is a primitive element.- Intermediate field — a field $K$ with $F \subseteq K \subseteq E$.
Examples: $[\mathbb{C} : \mathbb{R}] = 2$ with basis $1, i$. $[\mathbb{Q}(\sqrt2) : \mathbb{Q}] = 2$ with basis $1, \sqrt2$, since $\mathbb{Q}(\sqrt2) = \{a + b\sqrt2 : a, b \in \mathbb{Q}\}$ and $\sqrt2 \notin \mathbb{Q}$. $[\mathbb{R} : \mathbb{Q}]$ is infinite (a countable field cannot span the uncountable $\mathbb{R}$ with finitely many vectors). A field with $q$ elements that contains $\mathbb{F}p$ has $q = p^n$ where $n = [E : \mathbb{F}p]$, because an $n$-dimensional vector space over $\mathbb{F}p$ has $p^n$ elements.
The tower law
Theorem (tower law). If $F \subseteq K \subseteq E$ are fields, then
$$[E : F] = [E : K]\,[K : F],$$
in the sense that the left side is finite exactly when both factors are, and then equality holds.
Proof (finite case). Let $\alpha1, \dots, \alpham$ be a basis of $K$ over $F$ and $\beta1, \dots, \betan$ a basis of $E$ over $K$. The claim is that the $mn$ products $\alphai\betaj$ form a basis of $E$ over $F$.
- Spanning. Any $e \in E$ is $\sumj kj \betaj$ with $kj \in K$, and each $kj = \sumi f{ij}\alphai$ with $f{ij} \in F$. So $e = \sum{i,j} f{ij}\,\alphai\betaj$.- Independence. If $\sum{i,j} f{ij}\alphai\betaj = 0$, regroup as $\sumj \big(\sumi f{ij}\alphai\big)\betaj = 0$. The inner sums are in $K$ and the $\betaj$ are independent over $K$, so each inner sum is $0$. The $\alphai$ are independent over $F$, so every $f{ij} = 0$. ∎
Corollaries. (1) If $K$ is intermediate in a finite extension $E/F$, then $[K : F]$ divides $[E : F]$. (2) If $[E : F]$ is prime, there is no intermediate field other than $F$ and $E$. (3) If $\alpha \in E$, then $[F(\alpha) : F]$ divides $[E : F]$.
> Trick: most degree computations are "bound above by a tower, bound below by divisibility". If $[K : F] = 2$ and $[L : F] = 3$ are both inside $E$, then $6 \mid [E : F]$.
Chapter 2 — Algebraic elements and minimal polynomials
Terminology
- Algebraic — $\alpha \in E$ is algebraic over $F$ if $f(\alpha) = 0$ for some nonzero $f \in F[x]$. Otherwise $\alpha$ is transcendental over $F$.- Algebraic extension — every element of $E$ is algebraic over $F$.- Minimal polynomial — the monic polynomial $m\alpha \in F[x]$ of least degree with $m\alpha(\alpha) = 0$.
$\sqrt2$, $i$, $\sqrt[3]{2}$ and $e^{2\pi i/5}$ are algebraic over $\mathbb{Q}$. Hermite proved in 1873 that $e$ is transcendental and Lindemann in 1882 that $\pi$ is transcendental; both proofs are analytic and are not needed below.
The minimal polynomial
Theorem. Let $\alpha$ be algebraic over $F$. Then:
1. $m\alpha$ exists, is unique, and is irreducible over $F$;2. for $f \in F[x]$: $f(\alpha) = 0$ if and only if $m\alpha \mid f$;3. $F(\alpha) \cong F[x]/(m\alpha)$, so $[F(\alpha) : F] = \deg m\alpha$, with basis $1, \alpha, \dots, \alpha^{n-1}$ where $n = \deg m\alpha$.
Proof. Evaluation $\mathrm{ev}\alpha : F[x] \to E$, $f \mapsto f(\alpha)$, is a ring homomorphism. Its kernel $I$ is nonzero because $\alpha$ is algebraic, and $F[x]$ is a PID (post 04), so $I = (m)$ for a unique monic $m$; this $m$ has least degree among nonzero elements of $I$, so $m = m\alpha$, and statement 2 is "$f \in I \iff m\alpha \mid f$". The image of $\mathrm{ev}\alpha$ is a subring of the field $E$, hence a domain, so by the First Isomorphism Theorem $F[x]/(m\alpha)$ is a domain: $(m\alpha)$ is prime, so $m\alpha$ is irreducible. Then $(m\alpha)$ is maximal (post 04, Chapter 6), so the image $F[\alpha]$ is a field containing $F$ and $\alpha$; it is therefore $F(\alpha)$. The basis claim is the division algorithm: every class has a unique representative of degree $< n$. ∎
So to find $[F(\alpha) : F]$: find a polynomial with root $\alpha$ and prove it irreducible with the tests of post 04.
> Common pitfall: the minimal polynomial depends on the base field. $m{\sqrt2}$ over $\mathbb{Q}$ is $x^2 - 2$; over $\mathbb{R}$ it is $x - \sqrt2$.
Arithmetic in $\mathbb{Q}(\sqrt[3]{2})$. Let $a = \sqrt[3]{2}$, with $ma = x^3 - 2$ (Eisenstein at 2), so $[\mathbb{Q}(a) : \mathbb{Q}] = 3$ with basis $1, a, a^2$. To invert $1 + a$, use $(1 + a)(1 - a + a^2) = 1 + a^3 = 3$:
$$\frac{1}{1 + \sqrt[3]{2}} = \frac{1 - \sqrt[3]{2} + \sqrt[3]{4}}{3}.$$
Finite implies algebraic. If $[E : F] = n$ and $\alpha \in E$, the $n + 1$ elements $1, \alpha, \dots, \alpha^n$ are linearly dependent over $F$, and a dependence relation is a nonzero polynomial with root $\alpha$. Consequently, if $\alpha$ and $\beta$ are algebraic then $F(\alpha, \beta)$ is finite (tower law), so $\alpha \pm \beta$, $\alpha\beta$ and $\alpha/\beta$ are algebraic too: the algebraic numbers form a field.
Worked example: Q(√2, √3)
Degree. $[\mathbb{Q}(\sqrt2) : \mathbb{Q}] = 2$. Next, $\sqrt3 \notin \mathbb{Q}(\sqrt2)$: if $(a + b\sqrt2)^2 = 3$ with $a, b \in \mathbb{Q}$, then $a^2 + 2b^2 = 3$ and $2ab = 0$. $b = 0$ gives $a^2 = 3$ and $a = 0$ gives $b^2 = 3/2$, neither possible in $\mathbb{Q}$. So $x^2 - 3$ stays irreducible over $\mathbb{Q}(\sqrt2)$, and by the tower law
$$[\mathbb{Q}(\sqrt2, \sqrt3) : \mathbb{Q}] = [\mathbb{Q}(\sqrt2, \sqrt3) : \mathbb{Q}(\sqrt2)]\,[\mathbb{Q}(\sqrt2) : \mathbb{Q}] = 2 \cdot 2 = 4,$$
with basis $1, \sqrt2, \sqrt3, \sqrt6$ (products of the two bases, as in the proof of the tower law).
A primitive element. Let $\theta = \sqrt2 + \sqrt3$. Then $\theta^2 = 5 + 2\sqrt6$, so $(\theta^2 - 5)^2 = 24$, that is
$$\theta^4 - 10\theta^2 + 1 = 0.$$
Also $\theta^3 = 11\sqrt2 + 9\sqrt3$, which gives $\sqrt2 = (\theta^3 - 9\theta)/2$ and $\sqrt3 = (11\theta - \theta^3)/2$. So $\mathbb{Q}(\theta)$ contains $\sqrt2$ and $\sqrt3$, hence $\mathbb{Q}(\theta) = \mathbb{Q}(\sqrt2, \sqrt3)$ has degree 4. A degree-4 monic polynomial with root $\theta$ must then be the minimal polynomial: $m\theta = x^4 - 10x^2 + 1$, irreducible over $\mathbb{Q}$ (its four roots are $\pm\sqrt2 \pm \sqrt3$).
This is an instance of the primitive element theorem: every finite separable extension (Chapter 5; in particular every finite extension of $\mathbb{Q}$) is simple.
Chapter 3 — Ruler and compass
Start with the points $0$ and $1$ in the plane $\mathbb{C}$. A ruler draws the line through two constructed points; a compass draws a circle with a constructed centre through a constructed point. New points are intersections of lines and circles. A real number is constructible if it is a coordinate of a constructible point.
Key observation. If all points so far have coordinates in a field $K$, then:
- the intersection of two lines solves a linear system, so it has coordinates in $K$;- an intersection involving a circle solves a quadratic equation over $K$, so it has coordinates in $K$ or in some $K(\sqrt d)$ with $d \in K$, an extension of degree 2.
Theorem. If $\alpha$ is constructible, then $[\mathbb{Q}(\alpha) : \mathbb{Q}]$ is a power of 2.
Proof. By the observation, $\alpha$ lies in the top field of a tower $\mathbb{Q} = K0 \subset K1 \subset \cdots \subset Kr$ with each $[K{i+1} : Ki] = 2$. By the tower law $[Kr : \mathbb{Q}] = 2^r$, and $[\mathbb{Q}(\alpha) : \mathbb{Q}]$ divides it. ∎
Doubling the cube. A cube of volume 2 needs side $\sqrt[3]{2}$, whose minimal polynomial $x^3 - 2$ has degree 3. Not a power of 2, so impossible.
Trisecting 60°. A 60° angle is constructible (equilateral triangle). Trisecting it means constructing $c = \cos 20^\circ$. From $\cos 3\theta = 4\cos^3\theta - 3\cos\theta$ with $\theta = 20^\circ$ and $\cos 60^\circ = \tfrac12$:
$$4c^3 - 3c = \tfrac12 \quad\Longleftrightarrow\quad 8c^3 - 6c - 1 = 0.$$
By the rational root test, a rational root would be one of $\pm1, \pm\tfrac12, \pm\tfrac14, \pm\tfrac18$; substituting gives $1, -3, -3, 1, -\tfrac{19}{8}, \tfrac38, -\tfrac{111}{64}, -\tfrac{17}{64}$, never $0$. A cubic without rational roots is irreducible, so $[\mathbb{Q}(c) : \mathbb{Q}] = 3$ and the trisection is impossible. (Some angles, such as 90°, can be trisected; the theorem is that no method works for every angle.)
Squaring the circle. A square of area $\pi$ needs side $\sqrt\pi$. If $\sqrt\pi$ were constructible it would be algebraic, so $\pi$ would be algebraic, contradicting Lindemann.
> Watch out: "degree a power of 2" is necessary, not sufficient. The exact criterion is that the splitting field of the minimal polynomial has degree a power of 2. The Gauss–Wantzel theorem answers the polygon question: a regular $n$-gon is constructible if and only if $n$ is a power of 2 times a product of distinct Fermat primes $3, 5, 17, 257, 65537$.
Chapter 4 — Splitting fields and finite fields
Splitting fields
A splitting field of a nonconstant $f \in F[x]$ is an extension $E/F$ in which $f$ factors into linear factors, $f = c(x - \alpha1)\cdots(x - \alphan)$, and which is generated by the roots: $E = F(\alpha1, \dots, \alphan)$.
Theorem. Every nonconstant $f \in F[x]$ has a splitting field $E$, with $[E : F] \le n!$ where $n = \deg f$. Any two splitting fields of $f$ over $F$ are isomorphic by an isomorphism that fixes $F$.
(Existence: take an irreducible factor $p$ of $f$; in $F[x]/(p)$ it has a root (post 04), so $f$ gains a linear factor over an extension of degree $\le n$; repeat on the quotient, whose degree is $n - 1$. Uniqueness is proved by the same induction, extending an isomorphism one root at a time.)
Examples:
- $x^2 - 2$ over $\mathbb{Q}$: splitting field $\mathbb{Q}(\sqrt2)$, degree 2.- $x^4 - 10x^2 + 1$: roots $\pm\sqrt2 \pm \sqrt3$, splitting field $\mathbb{Q}(\sqrt2, \sqrt3)$, degree 4.- $x^3 - 2$: roots $\alpha$, $\alpha\omega$, $\alpha\omega^2$ with $\alpha = \sqrt[3]{2}$ and $\omega = e^{2\pi i/3}$, a root of $x^2 + x + 1$. The splitting field is $E = \mathbb{Q}(\alpha, \omega)$ (it contains $\omega = \alpha\omega/\alpha$). $[\mathbb{Q}(\alpha) : \mathbb{Q}] = 3$, and $\omega \notin \mathbb{Q}(\alpha) \subset \mathbb{R}$, so $[E : \mathbb{Q}(\alpha)] = 2$. Hence $[E : \mathbb{Q}] = 6 = 3!$, the maximum.
Finite fields
Let $E$ be a finite field. Its characteristic is a prime $p$ (post 04), so it contains $\mathbb{F}p$ and has $p^n$ elements, $n = [E : \mathbb{F}p]$.
Lemma (freshman's dream). In a commutative ring of characteristic $p$, $(a + b)^p = a^p + b^p$.
Proof. Expand with the binomial theorem. For $0 < k < p$, $\binom{p}{k} = \frac{p!}{k!(p-k)!}$ is divisible by $p$: $p$ divides the numerator but not the denominator. So all middle terms vanish. ∎
Hence the Frobenius map $\mathrm{Fr}(a) = a^p$ is a ring homomorphism. On a field it is injective (post 04: homomorphisms out of fields are injective), so on a finite field it is an automorphism, and it fixes exactly the elements of $\mathbb{F}p$ (the roots of $x^p - x$; there are at most $p$ of them, and all of $\mathbb{F}p$ qualifies by Fermat's little theorem from post 00).
Theorem (existence and uniqueness of finite fields). For every prime $p$ and $n \ge 1$ there is a field with $q = p^n$ elements, and any two such fields are isomorphic. It is written $\mathbb{F}q$.
Proof. Existence. Let $E$ be a splitting field of $g = x^q - x$ over $\mathbb{F}p$, and $S$ the set of roots of $g$ in $E$. The derivative $g' = qx^{q-1} - 1 = -1$ has no common root with $g$, so the roots are distinct and $|S| = q$. $S$ is a subfield: if $a^q = a$ and $b^q = b$, then $(a + b)^q = a^q + b^q = a + b$ (apply the lemma $n$ times), $(ab)^q = ab$, and $(a^{-1})^q = a^{-1}$. A subfield containing all roots is the whole splitting field, so $E = S$ has $q$ elements.
Uniqueness. In any field $K$ with $q$ elements, the multiplicative group has order $q - 1$, so $a^{q-1} = 1$ for $a \ne 0$ by Lagrange, and $a^q = a$ for all $a$. So $K$ consists of the $q$ roots of $x^q - x$, and is a splitting field of $x^q - x$ over $\mathbb{F}p$. Splitting fields are unique up to isomorphism. ∎
Two further facts, stated without proof: the multiplicative group $\mathbb{F}q^\times$ is cyclic (post 04's $\mathbb{F}8$ table showed a generator), and $\mathbb{F}{p^d}$ sits inside $\mathbb{F}{p^n}$ exactly when $d \mid n$.
Worked example. Over $\mathbb{F}2$:
$$\begin{aligned}x^4 - x &= x\,(x + 1)\,(x^2 + x + 1), \\x^8 - x &= x\,(x + 1)\,(x^3 + x + 1)\,(x^3 + x^2 + 1).\end{aligned}$$
The first line lists the minimal polynomials of the 4 elements of $\mathbb{F}4$; the second those of the 8 elements of $\mathbb{F}8$. The two cubics are the two irreducible cubics over $\mathbb{F}2$, each with three roots in $\mathbb{F}8$. No quadratic appears in the second line, matching $2 \nmid 3$: $\mathbb{F}4 \not\subseteq \mathbb{F}8$.
Chapter 5 — Galois extensions and the Galois group
Terminology
- $F$-automorphism of $E$ — a field automorphism $\sigma : E \to E$ with $\sigma(a) = a$ for every $a \in F$. They form a group $\operatorname{Aut}(E/F)$.- Fixed field — for a subgroup $H \le \operatorname{Aut}(E)$, $E^H = \{ e \in E : \sigma(e) = e \text{ for all } \sigma \in H \}$.- Separable polynomial — one with no repeated root in its splitting field. An algebraic extension is separable if every minimal polynomial is separable.- Normal extension — an algebraic $E/F$ such that every irreducible polynomial in $F[x]$ with one root in $E$ splits completely in $E$.- Galois extension — a finite extension that is normal and separable. Its Galois group is $\operatorname{Gal}(E/F) = \operatorname{Aut}(E/F)$.- Galois group of a polynomial — $\operatorname{Gal}(E/F)$ for $E$ a splitting field of the (separable) polynomial.
Separability is automatic in the cases you meet most. Over a field of characteristic 0, and over any finite field, every irreducible polynomial is separable. (An irreducible $f$ has a repeated root only if $\gcd(f, f') \ne 1$, which forces $f' = 0$; in characteristic 0 that is impossible for nonconstant $f$.) The standard inseparable example is $x^p - t$ over the field $\mathbb{F}p(t)$ of rational functions: it is irreducible (Eisenstein at the prime $t$), but over its splitting field it equals $(x - t^{1/p})^p$.
Automorphisms permute roots
Lemma. If $f \in F[x]$, $\alpha \in E$ is a root of $f$, and $\sigma \in \operatorname{Aut}(E/F)$, then $\sigma(\alpha)$ is a root of $f$.
Proof. If $f = \sum ck x^k$ with $ck \in F$, then $f(\sigma(\alpha)) = \sum ck \sigma(\alpha)^k = \sigma\big(\sum ck\alpha^k\big) = \sigma(0) = 0$, since $\sigma$ fixes each $ck$. ∎
So if $E = F(\alpha1, \dots, \alphan)$ is a splitting field of $f$, an $F$-automorphism is determined by how it permutes $\alpha1, \dots, \alphan$, and $\operatorname{Aut}(E/F)$ is (isomorphic to) a subgroup of $Sn$, an action on the roots in the sense of post 03.
Theorem (characterisations of Galois extensions). For a finite extension $E/F$ the following are equivalent:
1. $E/F$ is Galois (normal and separable);2. $E$ is the splitting field over $F$ of a separable polynomial;3. $\lvert\operatorname{Aut}(E/F)\rvert = [E : F]$;4. the fixed field of $\operatorname{Aut}(E/F)$ is exactly $F$.
In general $\lvert\operatorname{Aut}(E/F)\rvert \le [E : F]$ and equality is what "Galois" means in practice.
Examples.
- $\operatorname{Gal}(\mathbb{C}/\mathbb{R}) = \{\mathrm{id}, \text{complex conjugation}\} \cong \mathbb{Z}/2\mathbb{Z}$.- $\mathbb{Q}(\sqrt[3]{2})/\mathbb{Q}$ is not Galois. An automorphism must send $\sqrt[3]{2}$ to a root of $x^3 - 2$ inside $\mathbb{Q}(\sqrt[3]{2}) \subset \mathbb{R}$, and the only real root is $\sqrt[3]{2}$ itself. So $\operatorname{Aut}$ is trivial, of order $1 < 3$. It fails normality: $x^3 - 2$ has one root in the field but does not split there.- $\operatorname{Gal}(\mathbb{F}{p^n}/\mathbb{F}p)$ is cyclic of order $n$, generated by Frobenius. ($\mathrm{Fr}^k$ fixes exactly the roots of $x^{p^k} - x$, so $\mathrm{Fr}$ has order exactly $n$; and the group has order $[\mathbb{F}{p^n} : \mathbb{F}p] = n$ because $\mathbb{F}{p^n}$ is the splitting field of the separable $x^{p^n} - x$.)
Chapter 6 — The Fundamental Theorem of Galois Theory
Theorem (Fundamental Theorem of Galois Theory). Let $E/F$ be a finite Galois extension with group $G = \operatorname{Gal}(E/F)$. Then the maps
$$H \longmapsto E^H \qquad\text{and}\qquad K \longmapsto \operatorname{Gal}(E/K)$$
are mutually inverse bijections between subgroups $H \le G$ and intermediate fields $F \subseteq K \subseteq E$, and:
1. they reverse inclusion: $H1 \le H2$ if and only if $E^{H1} \supseteq E^{H2}$;2. degrees match indices: $[E : E^H] = |H|$ and $[E^H : F] = [G : H]$;3. $K/F$ is Galois if and only if $\operatorname{Gal}(E/K)$ is a normal subgroup of $G$, and then $\operatorname{Gal}(K/F) \cong G / \operatorname{Gal}(E/K)$.
So the lattice of intermediate fields is the lattice of subgroups turned upside down: the big field $E$ matches the trivial group, the base $F$ matches all of $G$. Part 3 is where normal subgroups and quotient groups (post 02) acquire their original meaning.
Worked example 1: Q(√2, √3)
$E = \mathbb{Q}(\sqrt2, \sqrt3)$ is the splitting field of the separable $(x^2 - 2)(x^2 - 3)$, so it is Galois of degree 4 and $G$ has 4 elements. Each automorphism sends $\sqrt2 \mapsto \pm\sqrt2$ and $\sqrt3 \mapsto \pm\sqrt3$, and all four sign choices occur. Let
$$\sigma : \sqrt2 \mapsto -\sqrt2,\ \sqrt3 \mapsto \sqrt3, \qquad \tau : \sqrt2 \mapsto \sqrt2,\ \sqrt3 \mapsto -\sqrt3.$$
Each has order 2 and they commute, so $G = \{\mathrm{id}, \sigma, \tau, \sigma\tau\} \cong \mathbb{Z}/2 \times \mathbb{Z}/2$. The subgroups and their fixed fields:
| Subgroup | Order | Fixed field | Degree over $\mathbb{Q}$ ||---|---|---|---|| $\{\mathrm{id}\}$ | 1 | $\mathbb{Q}(\sqrt2, \sqrt3)$ | 4 || $\langle\sigma\rangle$ | 2 | $\mathbb{Q}(\sqrt3)$ | 2 || $\langle\tau\rangle$ | 2 | $\mathbb{Q}(\sqrt2)$ | 2 || $\langle\sigma\tau\rangle$ | 2 | $\mathbb{Q}(\sqrt6)$ | 2 || $G$ | 4 | $\mathbb{Q}$ | 1 |
($\sigma\tau(\sqrt6) = (-\sqrt2)(-\sqrt3) = \sqrt6$.) By the theorem these are all the intermediate fields: exactly three quadratic subfields, which is not obvious without Galois theory. $G$ is abelian, so every subgroup is normal and every intermediate field is Galois over $\mathbb{Q}$.
Worked example 2: the splitting field of x³ − 2
$E = \mathbb{Q}(\alpha, \omega)$ with $\alpha = \sqrt[3]{2}$, of degree 6 (Chapter 4). It is Galois, so $|G| = 6$, and $G$ acts on the three roots $\alpha, \alpha\omega, \alpha\omega^2$, so $G$ is a subgroup of $S3$ of order 6: $G \cong S3$. Concretely an automorphism is fixed by $\alpha \mapsto \alpha\omega^a$ ($a \in \{0, 1, 2\}$) and $\omega \mapsto \omega$ or $\omega^2$. Let
$$\sigma : \alpha \mapsto \alpha\omega,\ \omega \mapsto \omega \quad(\text{order } 3), \qquad \tau : \alpha \mapsto \alpha,\ \omega \mapsto \omega^2 \quad(\text{complex conjugation, order } 2).$$
Composing right to left (apply $\tau$ first in $\sigma\tau$): $\sigma\tau$ sends $\alpha \mapsto \alpha\omega$ and $\omega \mapsto \omega^2$, so $\sigma\tau(\alpha\omega^2) = \alpha\omega \cdot \omega^4 = \alpha\omega^2$. Similarly $\sigma^2\tau$ fixes $\alpha\omega$. The six subgroups of $S3$ and their fixed fields:
| Subgroup | Order | Normal? | Fixed field | Degree over $\mathbb{Q}$ ||---|---|---|---|---|| $\{\mathrm{id}\}$ | 1 | yes | $\mathbb{Q}(\alpha, \omega)$ | 6 || $\langle\tau\rangle$ | 2 | no | $\mathbb{Q}(\alpha)$ | 3 || $\langle\sigma\tau\rangle$ | 2 | no | $\mathbb{Q}(\alpha\omega^2)$ | 3 || $\langle\sigma^2\tau\rangle$ | 2 | no | $\mathbb{Q}(\alpha\omega)$ | 3 || $\langle\sigma\rangle = A3$ | 3 | yes | $\mathbb{Q}(\omega)$ | 2 || $S3$ | 6 | yes | $\mathbb{Q}$ | 1 |
Each order-2 subgroup fixes one real or complex cube root, so its fixed field contains that root; the degree count $[G : H] = 3 = [\mathbb{Q}(\alpha\omega^k) : \mathbb{Q}]$ makes it equal to $\mathbb{Q}(\alpha\omega^k)$. $\sigma$ fixes $\omega$, and $[G : \langle\sigma\rangle] = 2 = [\mathbb{Q}(\omega) : \mathbb{Q}]$.
Part 3 of the theorem, checked on this example: $A3 = \langle\sigma\rangle$ is normal, and $\mathbb{Q}(\omega)$ is Galois over $\mathbb{Q}$ (it is the splitting field of $x^2 + x + 1$), with $\operatorname{Gal}(\mathbb{Q}(\omega)/\mathbb{Q}) \cong S3/A3 \cong \mathbb{Z}/2$. The three subgroups of order 2 are not normal (they are conjugate, like the three Sylow 2-subgroups of $S3$ in post 03), and correspondingly $\mathbb{Q}(\sqrt[3]{2})$ and its two conjugate fields are not Galois over $\mathbb{Q}$.
> Common pitfall: the correspondence reverses order. A bigger subgroup fixes more conditions and so has a smaller fixed field. Draw the two lattices with $E$ at the top and $\{\mathrm{id}\}$ at the top.
Chapter 7 — Solvable groups and the quintic
Solvable groups
A group $G$ is solvable if there is a chain of subgroups
$$\{e\} = G0 \trianglelefteq G1 \trianglelefteq \cdots \trianglelefteq Gk = G$$
with each $Gi$ normal in $G{i+1}$ and each quotient $G{i+1}/Gi$ abelian.
- Abelian groups are solvable ($k = 1$).- $S3$: $\{e\} \trianglelefteq A3 \trianglelefteq S3$ with quotients $\mathbb{Z}/3$ and $\mathbb{Z}/2$.- $S4$: $\{e\} \trianglelefteq V4 \trianglelefteq A4 \trianglelefteq S4$ with quotients $\mathbb{Z}/2 \times \mathbb{Z}/2$, $\mathbb{Z}/3$, $\mathbb{Z}/2$. So every subgroup of $S4$ is solvable too (subgroups and quotients of solvable groups are solvable).- $S5$ is not solvable. Its only normal subgroups are $\{e\}$, $A5$, $S5$, and $A5$ is simple and non-abelian (post 02). A chain would have to pass through $A5$, and the step below $A5$ would need a normal subgroup of $A5$ with abelian quotient; the only candidates are $A5$ itself (quotient trivial, no progress) and $\{e\}$ (quotient $A5$, not abelian). Equivalently, the commutator subgroup of $A5$ is $A5$, so the derived series $S5 \supset A5 \supset A5 \supset \cdots$ never reaches $\{e\}$.
Radicals
A polynomial $f \in \mathbb{Q}[x]$ is solvable by radicals if its roots can be written starting from rational numbers using $+, -, \times, \div$ and $k$-th roots. The quadratic formula shows this for degree 2; Cardano's and Ferrari's formulas do it for degrees 3 and 4.
Theorem (Galois, characteristic 0). $f$ is solvable by radicals if and only if its Galois group is solvable.
(Idea of "only if": adjoining a $k$-th root to a field that already contains the $k$-th roots of unity gives a Galois extension with cyclic group; adjoining roots of unity gives an abelian group. A tower of such steps gives, through part 3 of the Fundamental Theorem, a chain of normal subgroups with abelian quotients.)
Since $S4$ is solvable and the Galois group of a degree-$n$ polynomial embeds in $Sn$, every polynomial of degree $\le 4$ is solvable by radicals. The Abel–Ruffini theorem says the general quintic is not: the generic polynomial $x^5 + a4x^4 + \cdots + a0$ with independent indeterminate coefficients has Galois group $S5$ over $\mathbb{Q}(a0, \dots, a4)$, which is not solvable, so there is no formula in radicals valid for all quintics. Galois theory gives more: specific quintics with rational coefficients have unsolvable roots.
A concrete unsolvable quintic
Lemma. Let $p$ be prime and $f \in \mathbb{Q}[x]$ irreducible of degree $p$ with exactly two non-real roots. Then $\operatorname{Gal}(f) \cong Sp$.
Proof. View $G = \operatorname{Gal}(f)$ as a subgroup of $Sp$ acting on the $p$ roots. For a root $\alpha$, $[\mathbb{Q}(\alpha) : \mathbb{Q}] = p$ divides $[E : \mathbb{Q}] = |G|$, so by Cauchy's theorem (post 03) $G$ has an element of order $p$; in $Sp$ the elements of order $p$ are the $p$-cycles. Complex conjugation restricts to an element of $G$ that swaps the two non-real roots and fixes the $p - 2$ real ones: a transposition. For prime $p$, a $p$-cycle and a transposition generate all of $Sp$. ∎
Theorem. $f(x) = x^5 - 4x + 2$ is not solvable by radicals over $\mathbb{Q}$.
Proof. Irreducible: Eisenstein at $p = 2$ ($2 \mid -4$, $2 \mid 2$, $4 \nmid 2$, leading coefficient 1).
Exactly three real roots: $f'(x) = 5x^4 - 4$ vanishes only at $x = \pm c$ with $c = (4/5)^{1/4} \approx 0.9457$. So $f$ increases, then decreases on $[-c, c]$, then increases. The local maximum $f(-c) \approx 5.03 > 0$ and the local minimum $f(c) \approx -1.03 < 0$, while $f \to -\infty$ as $x \to -\infty$ and $f \to +\infty$ as $x \to +\infty$. So $f$ crosses zero once on each of the three monotone pieces: three real roots, approximately $-1.5185$, $0.5085$ and $1.2436$, and the other two roots are a complex-conjugate pair.
By the lemma with $p = 5$, $\operatorname{Gal}(f) \cong S5$, which is not solvable. By Galois's theorem, $f$ is not solvable by radicals. ∎
> Watch out: "not solvable by radicals" does not mean the roots are hard to compute. Numerically they are easy (the values above). It means there is no finite expression in $+, -, \times, \div, \sqrt[k]{\ }$ for them.
Practice Exercises
1. Find $[\mathbb{Q}(\sqrt2, \sqrt[3]{2}) : \mathbb{Q}]$.
<details><summary>Solution</summary>
$\mathbb{Q}(\sqrt2)$ has degree 2 and $\mathbb{Q}(\sqrt[3]{2})$ has degree 3, both intermediate, so 2 and 3 divide the degree, and so does 6. On the other hand $[\mathbb{Q}(\sqrt2, \sqrt[3]{2}) : \mathbb{Q}(\sqrt[3]{2})] \le 2$ (root of $x^2 - 2$), so the degree is at most $2 \cdot 3 = 6$. Hence it is exactly 6.
</details>
2. Find the minimal polynomial of $\sqrt2 + i$ over $\mathbb{Q}$.
<details><summary>Solution</summary>
Let $\theta = \sqrt2 + i$. Then $\theta^2 = 1 + 2\sqrt2\,i$, so $(\theta^2 - 1)^2 = -8$, giving $\theta^4 - 2\theta^2 + 9 = 0$. As in the $\sqrt2 + \sqrt3$ example, $\mathbb{Q}(\theta) = \mathbb{Q}(\sqrt2, i)$, which has degree 4 ($i \notin \mathbb{Q}(\sqrt2) \subset \mathbb{R}$). So $m\theta = x^4 - 2x^2 + 9$.
</details>
3. Show that the regular 7-gon is not constructible, using that $2\cos(2\pi/7)$ is a root of $x^3 + x^2 - 2x - 1$.
<details><summary>Solution</summary>
Rational roots could only be $\pm 1$, which give $-1$ and $1$, so the cubic has no rational root and is irreducible. Then $[\mathbb{Q}(2\cos(2\pi/7)) : \mathbb{Q}] = 3$, not a power of 2, so $\cos(2\pi/7)$ is not constructible, and neither is the 7-gon. (Consistent with Gauss–Wantzel: 7 is prime but not a Fermat prime.)
</details>
4. List all subfields of $\mathbb{F}{64}$ and draw their inclusions. What is $\operatorname{Gal}(\mathbb{F}{64}/\mathbb{F}2)$?
<details><summary>Solution</summary>
$64 = 2^6$ and the divisors of 6 are 1, 2, 3, 6, so the subfields are $\mathbb{F}2, \mathbb{F}4, \mathbb{F}8, \mathbb{F}{64}$. Inclusions: $\mathbb{F}2 \subset \mathbb{F}4 \subset \mathbb{F}{64}$ and $\mathbb{F}2 \subset \mathbb{F}8 \subset \mathbb{F}{64}$, while $\mathbb{F}4 \not\subseteq \mathbb{F}8$. The Galois group is cyclic of order 6, generated by Frobenius; its subgroups (orders 1, 2, 3, 6) match the subfields $\mathbb{F}{64}, \mathbb{F}8, \mathbb{F}4, \mathbb{F}2$ in that order.
</details>
5. In $\mathbb{Q}(\sqrt2, \sqrt3)$, which automorphism fixes $\sqrt6$, and what is the fixed field of $\{\mathrm{id}, \sigma\}$? Why does the Fundamental Theorem guarantee that $\mathbb{Q}(\sqrt2 + \sqrt3)$ is the whole field without computing $\theta^3$?
<details><summary>Solution</summary>
$\sigma\tau$ fixes $\sqrt6$ (both signs flip). $\sigma$ fixes $\sqrt3$, so $E^{\langle\sigma\rangle} \supseteq \mathbb{Q}(\sqrt3)$, and both have degree 2, so they are equal. For $\theta = \sqrt2 + \sqrt3$: the non-identity automorphisms send $\theta$ to $-\sqrt2 + \sqrt3$, $\sqrt2 - \sqrt3$, $-\sqrt2 - \sqrt3$, all different from $\theta$. So only the trivial subgroup fixes $\theta$, and $\mathbb{Q}(\theta)$ corresponds to $\{\mathrm{id}\}$, i.e. it is all of $E$.
</details>
6. Explain why every polynomial of degree at most 4 over $\mathbb{Q}$ is solvable by radicals, but this argument says nothing about degree 5.
<details><summary>Solution</summary>
The Galois group of a degree-$n$ polynomial is a subgroup of $Sn$. For $n \le 4$, $Sn$ is solvable (chains above), and subgroups of solvable groups are solvable, so Galois's theorem applies. For $n = 5$ the group may be a solvable subgroup of $S5$ (for example $x^5 - 2$ is solvable by radicals, since $\sqrt[5]{2}$ is already a radical), or all of $S5$, as for $x^5 - 4x + 2$. The degree alone does not decide.
</details>
Cheat-sheet
| Concept | Key fact ||---|---|| Degree | $[E : F] = \dimF E$ || Tower law | $[E : F] = [E : K][K : F]$ || Minimal polynomial | monic, irreducible, divides every $f$ with $f(\alpha) = 0$ || Simple extension | $F(\alpha) \cong F[x]/(m\alpha)$, degree $\deg m\alpha$ || $\mathbb{Q}(\sqrt2, \sqrt3)$ | degree 4, primitive element $\sqrt2 + \sqrt3$, min poly $x^4 - 10x^2 + 1$ || Constructible | degree over $\mathbb{Q}$ is a power of 2 (necessary) || Impossible constructions | $\sqrt[3]{2}$ (degree 3), $\cos 20^\circ$ (root of $8x^3 - 6x - 1$), $\sqrt\pi$ || Splitting field | exists, unique up to isomorphism, degree at most $n!$ || Finite fields | one field of each order $p^n$: the roots of $x^{p^n} - x$ || Frobenius | $a \mapsto a^p$; generates $\operatorname{Gal}(\mathbb{F}{p^n}/\mathbb{F}p) \cong \mathbb{Z}/n$ || Galois extension | normal and separable; $\lvert\operatorname{Aut}(E/F)\rvert = [E : F]$ || Fundamental Theorem | subgroups ↔ intermediate fields, order-reversing; normal ↔ Galois || $x^3 - 2$ | splitting field degree 6, group $S3$, subfield $\mathbb{Q}(\omega)$ ↔ $A3$ || Solvable group | chain of normal steps with abelian quotients; $S4$ yes, $S5$ no || Galois's criterion | solvable by radicals ⇔ solvable Galois group (char 0) || Unsolvable quintic | $x^5 - 4x + 2$: Eisenstein at 2, three real roots, group $S5$ |
What's next
This post completes the core course: from the integers and modular arithmetic (post 00), through groups, actions and Sylow theory (posts 01–03), rings and fields (post 04), to Galois theory. The table below lists the planned extensions. Each builds directly on the material here, so the series can grow without renumbering the existing posts.
Planned future extensions
| Post | Topic | What it covers ||---|---|---|| 06 | Modules and linear algebra over rings | Modules, free modules, the structure theorem for finitely generated modules over a PID; the finite abelian groups of post 03 and the rational and Jordan canonical forms as two special cases || 07 | Representation theory of finite groups | Group representations, Maschke's theorem, Schur's lemma, characters and orthogonality, character tables of $S3$, $S4$ and $A5$ || 08 | Algebraic number theory | Rings of integers, Dedekind domains, how unique factorisation of ideals repairs $\mathbb{Z}[\sqrt{-5}]$, the class group, cyclotomic fields || 09 | Commutative algebra and a first look at algebraic geometry | Noetherian rings, the Hilbert basis theorem, localisation, the Nullstellensatz, varieties and their coordinate rings || 10 | Algebra in cryptography | Finite-field arithmetic in practice (AES uses $\mathbb{F}{2^8}$), Diffie–Hellman and discrete logarithms, elliptic-curve groups, lattices and lattice-based cryptography |
References
- D. S. Dummit and R. M. Foote, Abstract Algebra, 3rd edition, Wiley, 2004 — Chapters 13 (field extensions, constructions, splitting fields, finite fields) and 14 (Galois theory, finite fields, Galois groups of polynomials, solvability by radicals).- M. Artin, Algebra, 2nd edition, Pearson, 2011 — Chapters 15 (fields, ruler and compass) and 16 (Galois theory, quintic equations).- I. Stewart, Galois Theory, 4th edition, CRC Press, 2015 — a readable, historically informed development, including Abel–Ruffini.- J. S. Milne, Fields and Galois Theory, course notes (free).- E. Artin, Galois Theory, Notre Dame Mathematical Lectures 2, 1942 (Dover reprint) — the classic linear-algebra approach to the Fundamental Theorem.- MIT OpenCourseWare, 18.702 Algebra II (Spring 2011) — lecture notes on fields and Galois theory.- K. Conrad, Expository papers ("blurbs") — short notes on Galois theory, including Galois groups of cubics and quartics.
> Note: this post is in progress. Planned additions: a full proof of the Fundamental Theorem (Artin's lemma on fixed fields), cyclotomic extensions and the constructible 17-gon, the discriminant as a test for $\operatorname{Gal} \subseteq An$, and a worked Cardano solution of a cubic read through its Galois group.
AWS Course — 04 · Managed Data — RDS, Aurora, DynamoDB, ElastiCache
Now that compute can scale and survive (Post 03), the next question is where the data lives. AWS offers a long catalogue of managed data services. This post focuses on the four most common: RDS (managed relational), Aurora (RDS's cloud-native superset), DynamoDB (managed NoSQL key-value / document), and ElastiCache (managed Redis / Memcached). For each: when to choose it, the high-availability patterns, the backup story, and the most common cost surprises. A quick comparison to S3, Redshift, OpenSearch, and Neptune appears at the end so the decision tree is complete.
> Series map: 00 Foundations → 01 Network First → 02 Compute & Storage → 03 Scale & Survive → 04 Managed Data (this post) → 05 Serverless & Events → 06 Workflow Orchestration → 07 Observability & Security → 08 IaC.
Learning Outcome
- Choose between RDS, Aurora, DynamoDB, ElastiCache for a given workload.- Configure RDS Multi-AZ for HA and read replicas for read scaling.- Use Aurora's storage layer and serverless V2 mode.- Design a DynamoDB table with a sensible partition key + sort key + secondary indexes.- Configure DynamoDB capacity mode (on-demand vs provisioned + auto-scaling).- Place caching tiers correctly.- Recognise the major cost levers per service.
Chapter 1 — Pick the right data store
Quick decision tree:
The biggest mistake is choosing by familiarity rather than fit. Postgres on RDS is a fine default; DynamoDB is a much better fit for high-write key-value patterns. Picking wrong locks you into a costly migration later.
Chapter 2 — RDS
RDS (Relational Database Service) runs PostgreSQL, MySQL, MariaDB, Oracle, or SQL Server on EC2 + EBS, managed by AWS. You get:
- Daily automated backups + point-in-time recovery (1-35 days).- Multi-AZ failover (synchronous replica in another AZ; automatic promotion).- Up to 15 read replicas for read scaling.- Maintenance windows for patching.- IAM authentication, KMS encryption at rest, TLS in transit.
What you give up: full OS access. You can't run on an RDS host or install custom Postgres extensions outside AWS's approved list.
Engines and versions
| Engine | Typical use ||--------|-------------|| Postgres | most modern apps; richest feature set || MySQL | legacy or compatibility || MariaDB | MySQL drop-in || Oracle / SQL Server | licensed workloads |
Pick the most recent LTS major version for production unless a specific older feature is needed.
Multi-AZ vs read replica
| Feature | Multi-AZ | Read replica ||---------|----------|---------------|| Purpose | HA: automatic failover | Read scaling || Replication | synchronous | asynchronous || Visible? | invisible standby | endpoint queryable || Failover time | 60-120 sec | manual promote || Cost | 2× the instance | 1× per replica |
Production = always Multi-AZ. Heavy-read workloads = also one or more read replicas.
Backups
- Automated snapshots — every day, retained 1-35 days, free for active DBs.- Manual snapshots — kept until you delete them.- Point-in-time restore — recover to any second within the retention window (writes go to transaction logs in S3).
> Trick: RDS lets you spin up a new DB from a snapshot in seconds for testing. Great for "show me prod data from yesterday in a sandbox" workflows. Just remember to delete it after.
Chapter 3 — Aurora
Aurora is AWS's cloud-native rewrite of MySQL and PostgreSQL. Compatible at the wire level, but with a custom storage engine that:
- Replicates data across 6 copies in 3 AZs automatically.- Keeps working for writes after losing up to 2 copies, and for reads after losing up to 3.- Auto-grows storage in 10 GB chunks up to 128 TB.- Provides up to 15 read replicas that share the same storage layer (lag in single-digit ms).
Aurora vs RDS
| Trait | RDS | Aurora ||-------|-----|--------|| Engines | many | Postgres / MySQL compatible only || Storage | EBS (provisioned) | shared cluster volume, auto-grow || Throughput vs vanilla | baseline | up to 3-5× MySQL, 2-3× Postgres || Cost | cheaper at small scale | costlier baseline, cheaper at scale || Best for | low-traffic apps, small teams | production OLTP at scale, SaaS |
Aurora Serverless V2
Aurora Serverless V2 scales the compute capacity ( — Aurora Capacity Units) up and down with load, in fractions of an ACU. Used to be best for unpredictable workloads; in 2026 it's mature enough to be a viable default for new Aurora clusters.
Chapter 4 — DynamoDB
DynamoDB is a fully managed NoSQL key-value + document store. Single-digit millisecond latency at any scale. No servers to provision, no AZ design — AWS handles all of it.
Trade-offs vs SQL:- No joins (denormalise instead).- Limited transactional support (single-region transactions OK; multi-region eventual).- Query patterns must be designed first — DynamoDB tables are not "queried like SQL"; you access by key.
Table schema
A table has:- A partition key (mandatory) — determines which physical partition stores the item.- An optional sort key — orders items within a partition.- Items are JSON-like documents with arbitrary attributes.- Secondary indexes (Global or Local) let you query by other attributes.
Capacity modes
| Mode | When ||------|------|| On-demand | unpredictable traffic, dev, low-traffic | pay per request || Provisioned + auto-scaling | steady traffic with daily curve | cheaper at predictable scale |
Start with on-demand; switch to provisioned once you have a stable traffic profile and the cost gap matters.
Streams + change capture
DynamoDB Streams publishes every item-level change for 24 hours. Wire to Lambda for:- replication to another store (S3, OpenSearch, another region),- audit trails,- triggering downstream workflows.
Single-table design
Power users put multiple entity types in one table with carefully designed key patterns. Higher learning curve but lower cost and faster queries. Worth reading Alex DeBrie's The DynamoDB Book when ready.
Chapter 5 — ElastiCache
ElastiCache runs Redis or Memcached as a managed service. Two flavours:
| Engine | Best for ||--------|----------|| Redis (ElastiCache for Redis) | rich data structures, pub/sub, persistence, replicas, cluster mode | most use cases || Memcached | pure cache, multi-threaded, simpler | rarely chosen now |
Modern AWS also offers MemoryDB for Redis — Redis-compatible with multi-AZ durable storage (effectively Redis-as-a-database). Use when you need Redis semantics plus durability.
Cache patterns
- Cache-aside (lazy loading) — app reads from cache first; on miss, query DB and populate.- Write-through — every DB write also updates the cache.- TTL eviction — auto-expire entries after a duration.- Session store — sticky-session alternative for stateless app servers.- Rate limiting / leaderboards — natural Redis use cases.
> Trick: when adding a cache to fix a slow query, set a short TTL (60-300 sec) at first. Lets you measure hit rate and roll back without stale-data risk. Expand TTL once you're confident the data tolerates it.
Chapter 6 — Choosing between RDS, Aurora, DynamoDB
| Scenario | Pick ||----------|------|| Small SaaS, < 1k req/s, SQL | RDS Postgres Multi-AZ || Larger SaaS, 5k+ req/s, SQL | Aurora Postgres || Mobile app backend, key-value | DynamoDB on-demand || User-session store | ElastiCache Redis || Time-series IoT writes | DynamoDB or TimeStream || Reporting / analytics | Redshift or S3 + Athena || Search | OpenSearch || Caching API responses | ElastiCache Redis |
There is no universal best. Match the access pattern.
Chapter 7 — Costs and gotchas
| Item | Typical cost driver | Saver ||------|---------------------|-------|| RDS / Aurora instance | per-hour by class | reserved instance / 1-3 yr commit || RDS storage | per GB-month gp3 | rightsize; gp3 not gp2 || RDS backup beyond DB size | per GB-month | shorter retention or move old to S3 || RDS Multi-AZ | 2× the instance | required for production || DynamoDB on-demand | per request (read + write units) | provisioned + auto-scale at scale || DynamoDB storage | per GB-month | TTL old items || DynamoDB GSI | extra reads/writes | only create needed indexes || ElastiCache nodes | per-hour by class | reserved nodes; right-size || Cross-AZ traffic | per GB | keep app + DB in same AZ where safe |
Watch out: turning on performance insights, enhanced monitoring, automated backups at long retention, and many secondary indexes adds line items that double a small DB's bill quietly.
Chapter 8 — Migration shortcuts
- DMS (Database Migration Service) — continuous replication from one DB to another (on-prem → RDS, MySQL → Aurora, RDS → DynamoDB).- AWS Schema Conversion Tool (SCT) — convert schemas + stored procedures between engines.- / still work fine for small one-shot loads.
For production cut-overs, the DMS "ongoing-replication then cut" pattern is the safe playbook.
Practice Exercises
Exercise 1 — RDS Postgres Multi-AZ
Create an RDS Postgres Multi-AZ in the isolated subnets of the VPC from Post 01. Confirm:- the standby in another AZ exists,- the master endpoint resolves to the active node,- a forced failover () flips traffic within 60-120 sec.
Exercise 2 — Read replica
Add a read replica to the Exercise 1 DB. Point a read-only client (e.g., ) at the replica endpoint. Verify writes fail on the replica, succeed on the primary.
Exercise 3 — DynamoDB design
Design a DynamoDB table for a simple URL shortener:- partition key: (string)- attributes: , ,- secondary requirement: list 100 most recent short codes
What partition/sort key and (if needed) GSI satisfies both access patterns?
<details><summary>One valid answer</summary>
- Table partition key: (for direct lookup).- GSI: partition key (always literal ), sort key . Query the GSI with , sorted descending, limit 100.
Note the GSI pattern: cramming everything into one partition is fine at low scale but won't scale to massive write throughput; that's where you'd shard (e.g., by date).
</details>
Exercise 4 — Cache-aside
Add ElastiCache Redis in front of the RDS from Exercise 1. Implement a simple Python service that:- on checks Redis first,- on miss, queries Postgres, populates Redis with TTL 60 sec,- on writes Postgres and invalidates the Redis key.
Measure hit-rate after warm-up.
Exercise 5 — DynamoDB Streams to Lambda
Enable Streams on the table from Exercise 3. Hook up a Lambda that logs every change. Confirm in CloudWatch Logs.
Cheat-sheet
| What | How ||------|-----|| Default SQL | RDS Postgres Multi-AZ || SQL at scale | Aurora Postgres / MySQL || Key-value at scale | DynamoDB on-demand || Hot cache | ElastiCache Redis || Multi-AZ RDS | always for production || Read replicas | up to 15; async lag || RDS backup window | retain 1-35 days; PITR auto || Aurora storage | shared, auto-grows, 6 copies / 3 AZ || Aurora Serverless V2 | new clusters default candidate || DynamoDB key design | partition + optional sort || DynamoDB query model | by key, not arbitrary SQL || Streams | 24-hour change log → Lambda || Cache pattern | lazy load + TTL || Cost saver | RI for steady DBs, on-demand for spikes |
What's next
Post 05 introduces serverless and event-driven AWS — Lambda, API Gateway, SQS, SNS, EventBridge — the glue that connects everything from Posts 02-04 into asynchronous, scalable architectures.
References
- RDS docs- Aurora docs- DynamoDB Developer Guide- ElastiCache docs- The DynamoDB Book by Alex DeBrie — the canonical DynamoDB modelling guide.- AWS Whitepaper — Choosing the Right Database for Your Workload.
> Note: this post is in progress. Planned additions: Aurora Global Database walkthrough, DynamoDB single-table design example, OpenSearch + Redshift overview.














