From Account to Pod
0%

The Backend Ladder  ·  Deep dive  ·  Pillar C of three

From account
to pod.

Cloud fluency is knowing what happens between a user typing a hostname and your handler running — every hop, who authorised it, what it costs, and what breaks it. This page follows that path all the way down: IP ranges, IAM decisions, failover clocks, the scheduler, and the bill.

~9 hopsBefore your code runsDNS, CDN, load balancer, ingress, service, pod — each one a place to be misconfigured.
$0.01/GBCross-AZ, each wayCharged in both directions. Chatty services spread across zones pay twice for every byte.
60–120sRDS Multi-AZ failoverWrites are rejected for the duration. Your retry policy is the difference between a blip and an outage.
Part one

The whole journey

Start with the map. Every later section is one box on this diagram, opened up.

Cloud deep · 01

One request, every hop

Nine components sit between a user and your handler. Knowing the order lets you bisect an incident in one pass instead of guessing — and knowing what each hop fails like tells you which alert fired for a reason.

EDGE — OUTSIDE YOUR VPC Route 53 ~20ms, cached CloudFront TLS + edge cache fails as: NXDOMAIN, or stale records for the length of the TTL VPC EDGE ALB health-checked Ingress ctrl host/path rules Service label → endpoints fails as: 502 (no healthy targets), 503 (no capacity), 504 (backend too slow) INSIDE THE CLUSTER, INSIDE THE PRIVATE SUBNET kube-proxy / eBPF picks a ready pod IP your Go handler the only hop you wrote ElastiCache ~1ms same AZ RDS writer ~5–10ms indexed the hop nobody draws IAM authorised every one of these fails as: connection refused (pod not ready), timeout (pool exhausted), permission denied (role or security group) Bisect with the status code, not with a hunch 502 the load balancer has no healthy target — look at readiness probes and target group health, not at your code. 504 a target accepted and did not answer in time — that is your code, or something it waits on. Connection reset with no log line at all — a security group, a NetworkPolicy, or a pod that went away mid-request.
Only one box on this diagram contains code you wrote. That is the useful shock of cloud work: most incidents are configuration in the other eight. The corollary is that the fastest debugging tool is knowing which hop emits which symptom.
Part two

AWS essentials

AWS publishes well over two hundred services. You will meet about twenty-five. This part is the working set — what each one is for, and the handful of facts about each that decide whether your design works — followed by the two databases you will spend the most time with.

Cloud deep · 02

The twenty-five services you will actually use

Grouped by the job they do, not by the console's menu. If you can place a service in this grid and say one true thing about its limits, you know enough to start designing with it.

COMPUTE — RUN CODE Lambda per-request, 15 min cap ECS / Fargate containers, no nodes EKS Kubernetes, you run pods EC2 / ASG machines you patch Batch queued bulk jobs App Runner / Amplify smallest path to a URL DATA — KEEP IT S3 objects, 11 nines durable RDS / Aurora managed SQL DynamoDB key-value at any scale ElastiCache Redis / Valkey OpenSearch text search, logs EFS / EBS shared and block disks MOVE IT — NETWORK AND MESSAGING VPC your private network ALB / NLB L7 and L4 balancing Route 53 · CDN DNS and the edge SQS work queue, DLQ SNS / EventBridge fan-out and routing Kinesis / MSK ordered, replayable log GUARD IT AND WATCH IT IAM / STS who may do what KMS keys for encryption Secrets Manager rotated credentials CloudWatch metrics, logs, alarms CloudTrail who called which API X-Ray distributed traces Four of these carry almost every design A compute choice, a database choice, a queue, and IAM. The rest is supporting cast you add when a need appears — and a service you do not need is a cost and an on-call surface, not a free upgrade.
Read the grid by row, not by name. Each row is one of the four moves from the overview: run it, keep it, move it, guard it. When a design question arrives — "where should this live?" — the row tells you which four or five services are even candidates, which is most of the decision.
ServiceThe one thing to knowThe gotcha that bites
S3Effectively unlimited object storage, strongly consistent for reads after write.Request cost at high QPS, and a bucket policy that quietly makes it public.
LambdaScales to zero, pays per millisecond, 15-minute maximum.Cold starts, and one function's concurrency starving every other function in the account.
ECS FargateContainers with no nodes to patch — the low-ops default for a steady service.Slower to start than Lambda, and no bin-packing, so you pay per task.
EKSReal Kubernetes, portable, every knob available.You own upgrades, the CNI, IP planning, and a platform team's worth of work.
SQSDurable work queue with retries and a DLQ, no capacity to manage.Visibility timeout shorter than the handler means duplicate processing.
EventBridgeContent-based routing between services, plus schedules and SaaS events.At-least-once with no ordering; latency is fine but not sub-millisecond.
ElastiCacheManaged Redis or Valkey, sub-millisecond in the same AZ.A failover drops connections; the client must reconnect and the origin must survive the miss storm.
KMSKeys that never leave the service; every encrypt/decrypt is an API call.Request rate limits, and a key policy separate from IAM that must also allow you.
CloudWatchMetrics, log groups, alarms, dashboards — the default for everything.Ingestion cost, and custom metric cardinality that grows without limit.
Cloud deep · 03

RDS, feature by feature

RDS is a normal Postgres or MySQL server with the operations attached: backups, failover, patching, monitoring. Everything below is a switch you turn on — and most production incidents come from one that nobody turned on.

ONE RDS DEPLOYMENT, ALL THE MOVING PARTS app pods RDS Proxy pools connections PRIMARY · AZ-a reads + writes gp3, autoscaling disk STANDBY · AZ-b synchronous copy not readable Multi-AZ read replica · AZ-c async, has lag replica · other region promotable for DR backups → S3 daily snapshot + WAL ⇒ restore to any second, 1–35 days attached, and easy to forget parameter group · maintenance window · KMS encryption · Performance Insights The three that get confused with each other MULTI-AZ Availability. Synchronous standby you cannot read from. Failover 60–120s. Does nothing for read capacity. READ REPLICAS Throughput. Asynchronous, so reads can be stale by milliseconds or more. Not automatic failover. AURORA Different storage engine: 6 copies over 3 AZs, up to 15 readers, fast failover and near-zero replica lag.
The most common design error on this diagram is treating Multi-AZ as read scaling. It is not: the standby serves no traffic and exists purely to be promoted. If reads are your problem you want replicas, a cache, or Aurora — and if writes are your problem, none of these help and you are looking at sharding or a different store.
FeatureWhat it gives youWhat to actually do
Automated backups + PITRRestore to any second within a 1–35 day window.Set retention to at least 7 days, and rehearse a restore — an untested backup is a hypothesis.
Manual snapshotsKept until you delete them; survive instance deletion.Take one before every risky migration; automated backups die with the instance.
Multi-AZAutomatic failover to a synchronous standby.On for anything that matters. Budget 60–120s of failed writes and retry them.
Read replicasExtra read capacity; cross-region copies; promotable.Route only stale-tolerant reads there, and alert on replica lag.
RDS ProxyConnection pooling in front of the database; faster failover; IAM auth.Use it when many pods or Lambdas each hold a pool — it is the standard fix for connection exhaustion.
Parameter groupsServer configuration: max_connections, timeouts, logging.Never use the default group — you cannot edit it. Static parameters need a reboot.
Performance InsightsWhich queries consume database time, by wait event.Turn it on before you need it; it is the fastest route from "the DB is slow" to a query.
Storage autoscalingGrows the volume automatically up to a cap.Set it. A full disk takes the database read-only, and storage cannot be shrunk afterwards.
Encryption at rest (KMS)Transparent encryption of volume, snapshots and replicas.Enable at creation — you cannot turn it on later without a snapshot-restore migration.
Blue/green deploymentsA synced copy for major version upgrades, switched over in about a minute.The sane path for a major engine upgrade; the in-place alternative is a long outage.

The limit that catches teams first

max_connections scales with instance memory, not with your pod count. Forty pods × a pool of twenty is 800 connections against a db.t3.medium that allows roughly 450 — and Postgres spends real memory per connection. Size pools from Little's Law (concurrency = throughput × query time), not from a default, and put RDS Proxy in front once the fleet is large.

Cloud deep · 04

DynamoDB, feature by feature

DynamoDB gives you single-digit-millisecond reads at any scale, in exchange for deciding your queries before you create the table. Its features are mostly ways of buying back the flexibility that constraint costs you.

AN ITEM — SCHEMALESS EXCEPT FOR THE KEY PARTITION KEY CUST#42 SORT KEY ORDER#2026-08-19 ANY OTHER ATTRIBUTES total, status, ... Only the key is fixed. Item max 400KB. The sort key is what makes “this customer’s orders, newest first” one query. THE TABLE — HASHED ACROSS PARTITIONS YOU NEVER SEE each partition: ~3,000 RCU / 1,000 WCU ceiling GSI — a second copy own key, own capacity, eventually consistent, costs a write each time Streams → Lambda every change, in order per key — the built-in outbox for this database CAPACITY — TWO BILLING MODES, ONE PHYSICS ON-DEMAND Pay per request, absorbs spikes, nothing to tune. Start here. PROVISIONED + AUTOSCALING Cheaper for steady, predictable load. Scaling takes minutes. NEITHER SAVES A BAD KEY The per-partition ceiling applies in both modes. A hot key throttles either way. Query, never Scan Query goes straight to one partition by key and reads only what you asked for. Scan reads the whole table and then filters — you pay for every item examined. A Scan in a request path is the classic DynamoDB bill and latency story.
Every feature here exists to work around the single-key constraint. A GSI buys a second access pattern; Streams buy you the ability to react; DAX buys microsecond reads. What none of them buy is the ability to ask an unanticipated question cheaply — which is precisely what SQL is for.
FeatureWhat it gives youWhat to actually do
Partition key + sort keyOne query returns an ordered range of items under one key.Design keys from your access patterns first. Prefix sort keys (ORDER#, PROFILE#) to hold several entity types in one table.
GSIA second key on the same data — a different way to look things up.Project only the attributes you need; a GSI is a full copy, and every write to the base table writes here too.
LSISame partition key, alternative sort key, strongly consistent.Must be created with the table and caps that partition at 10GB. Prefer a GSI unless you specifically need strong consistency.
Read consistencyEventually consistent by default; strongly consistent on request (double cost).Use strong reads only where read-your-writes matters. GSIs cannot do strong reads at all.
Conditional writesattribute_not_exists or a version check, evaluated atomically.This is your optimistic lock and your idempotency guard — the same ledger trick as the foundations page.
TransactionsAll-or-nothing across up to 100 items, including across tables.Real, and twice the cost. Use for the few writes that genuinely must be atomic.
StreamsAn ordered change log per key, retained 24 hours, triggers Lambda.The native outbox: update the item and let the stream publish the event. No dual write.
TTLItems delete themselves after a timestamp attribute passes.Free expiry for sessions and carts — but deletion is best-effort within ~48 hours, so filter on read too.
PITRRestore the table to any second in the last 35 days.Turn it on. It restores to a new table, so plan the cutover.
DAXA DynamoDB-aware cache with microsecond reads.Only for read-heavy hot keys; it adds a cache-invalidation problem you did not previously have.
Global tablesMulti-region, active-active replication.Last-writer-wins conflict resolution — fine for profiles, wrong for balances.
Cloud deep · 05

Choosing between them, honestly

The real question is not scale — both scale further than most products need. It is whether you know your queries in advance, and whether you need to ask new ones later without a migration.

Can you list every query this data will ever need to answer? NO — or you are not sure → RDS / AURORA Joins, ad-hoc filters, reporting, a new index next quarter Also choose SQL when you need multi-row transactions, strong consistency everywhere, or an analyst with a query tool. This is most products, most of the time. YES, and they are key-shaped → DYNAMODB Sessions, carts, feeds, device state, event stores Also choose it for spiky traffic with no capacity planning, per-request billing at low volume, and workloads that must not have an instance to size. The honest default Start relational unless you have a specific reason not to. Postgres goes further than most teams expect, and it forgives the queries you did not anticipate. Reach for DynamoDB where the access pattern is genuinely fixed and the scale is real.
The regret runs one way. Teams that started on Postgres and later moved a hot table to DynamoDB describe it as a normal project. Teams that started on DynamoDB and later needed a query they had not designed for describe it as a rewrite. That asymmetry, not throughput, is the argument.
Cloud deep · 06

S3, feature by feature

S3 is the one service almost every design touches, and the one most people know least about. It is not a filesystem: it is a keyed object store with a price list, and nearly every S3 surprise — a bill, a leak, a stale read — comes from treating it as a disk.

THE KEY IS A STRING — THERE ARE NO FOLDERS s3://reports/2026/08/19/orders.csv One object, one key. The slashes are ordinary characters; there are no directories anywhere in the system. LIST with a prefix and delimiter=/ fakes one, paged, billed per 1,000 keys. PUT → GET is strongly consistent A successful PUT is visible to the next GET, and to the next LIST, in every region. The old read-after-write caveats are gone. What is still stale is whatever you cached in front. LIFECYCLE — THE SAME OBJECT, DESCENDING THROUGH CLASSES STANDARD hot data, day zero no retrieval fee dearest per GB INTELLIGENT-TIERING auto-tiers by access small monitoring fee unknown patterns STANDARD-IA instant, cheaper GB retrieval fee per GB 30-day minimum GLACIER INSTANT millisecond reads archive price per GB 90-day minimum GLACIER FLEXIBLE restore: 1 min to 12h you must ask first 90-day minimum DEEP ARCHIVE 12 hours, typically cheapest per GB 180-day minimum A lifecycle rule is a filter (prefix or tag) plus transitions and an expiry. It is evaluated once a day, asynchronously — a transition is never instant. Deleting early still bills the 30, 90 or 180-day minimum for that class. VERSIONING — A DELETE IS ANOTHER VERSION delete marker · latest GET returns 404 — nothing was erased v2 · 2026-08-19 14:02 v1 · 2026-08-01 09:20 still stored, still billed, restorable only ListObjectVersions shows it Expire noncurrent versions in the rule too, or the bucket grows forever. EVENT NOTIFICATIONS — S3 AS A TRIGGER s3:ObjectCreated:* filter by prefix + suffix Lambda — one per object SQS — buffer and retry EventBridge — routing Notifications are at-least-once and can be delayed. Not a transaction log. PRESIGNED URLS — THE BYTES NEVER TOUCH YOUR SERVICE browser your API signs locally 1 ask · 2 signed URL back, 15 min S3 bucket checks signature 3 The object bytes go browser → S3. Your service signs a string and never reads or forwards the payload.
The presigned flow is the one to internalise. A service that streams uploads through its own pods pays for the bandwidth, the memory and the request time, and falls over on a 2GB file. A service that signs a URL does constant work per upload regardless of object size — and the same trick, in reverse, serves private downloads without an authenticating proxy.
FeatureWhat it gives youWhat to actually do
Storage classesThe same object at a tenth or a twentieth of the price, at the cost of retrieval time and fees.Standard for anything served to users. Intelligent-Tiering when access is unpredictable and objects are over 128KB. Glacier tiers only for data you can wait for.
Lifecycle rulesAutomatic transitions and expiries by prefix or tag.Write one on day one: transition at 30 days, expire noncurrent versions at 30, abort incomplete multipart uploads at 7. That last rule is pure money.
VersioningEvery overwrite and delete is recoverable; a delete writes a marker rather than removing bytes.On for anything you would cry about. Then pair it with an expiry rule — without one, storage only grows, and deletions never reduce the bill.
Multipart uploadParallel parts, resumable, required above 5GB and sensible above ~100MB.Let the SDK’s transfer manager do it. Failed uploads leave orphaned parts you are billed for until a lifecycle rule aborts them.
Presigned URLsA time-limited, signed URL for one operation on one key.Use for both upload and download. The signature carries the signer’s permissions, so sign with a narrowly scoped role, and keep expiry in minutes.
Event notificationsObjectCreated / ObjectRemoved events to Lambda, SQS or EventBridge, filtered by prefix and suffix.Prefer SQS or EventBridge over direct Lambda for anything bursty — the queue is your buffer and your retry story.
SSE-S3 vs SSE-KMSBoth encrypt at rest. SSE-S3 is free and invisible; SSE-KMS gives you a key policy and an audit trail.SSE-KMS where you must prove who can decrypt. Turn on S3 Bucket Keys with it, or a high-volume prefix generates a KMS call per object and a real bill.
Block Public AccessAn account and bucket-level override that beats any policy or ACL.On, at the account level, everywhere. Serve public content through CloudFront with an origin access control instead of a public bucket.
Strong consistencyRead-after-write and read-after-list, in all regions, at no cost.Stop writing the retry loops that older guides teach. What still goes stale is your own cache and any CloudFront copy.
Replication (CRR/SRR)Asynchronous copies to another bucket or region, optionally with RTC.For DR and data residency. It replicates forward only — existing objects need a batch copy, and deletes do not propagate unless you say so.

Storage is cheap; requests and prefixes are where it goes wrong

S3 scales to about 3,500 PUT/COPY/POST/DELETE and 5,500 GET/HEAD requests per second per prefix, and it partitions on the key prefix — so a million small objects under one prefix throttle while the same million spread over ab/, cd/, ef/… do not. Cost follows the same shape: a terabyte sitting still is a few tens of dollars a month, but a million PUTs is roughly $5 and a million GETs roughly $0.40, so a chatty job that does one request per row will out-spend the storage it produces. Batch small objects, and never put a per-request S3 call inside a per-row loop.

Cloud deep · 07

SQS, SNS and EventBridge

Three services that all “send messages” and are not interchangeable. SQS is a work queue with one winner per message. SNS is a fan-out with no memory. EventBridge is a router that reads the message body. Picking the wrong one shows up months later as a reliability problem.

SQS — ONE CONSUMER WINS producer queue worker DLQ after maxReceiveCount one message, one consumer — never fan-out standard: at-least-once, unordered FIFO: ordered per MessageGroupId, and 300 calls/s, 3,000 msg/s when batched SNS — EVERY SUBSCRIBER publisher topic SQS queue Lambda HTTPS every subscriber gets every message, unless its filter policy rejects it first no retry state you can inspect — put an SQS queue between topic and consumer EVENTBRIDGE — ROUTING S3 / app bus rule → Lambda rule → SQS archive/replay rules match on the event body itself, not on a topic name you agreed on schedules, SaaS sources, cross-account buses, and replay out of the archive VISIBILITY TIMEOUT — WHERE DUPLICATE PROCESSING COMES FROM t=0 ReceiveMessage t=30s visibility expires t=45s handler finishes DeleteMessage rejected hidden from other consumers a second worker receives it two deliveries, one delete lost Set the timeout above your p99 handler time, call ChangeMessageVisibility for long jobs, and make the handler idempotent. At-least-once means the duplicate is your problem, not a fault in the queue. Which of the three, in one line each SQS One consumer must do the work, and the work must survive a crash. Gives you buffering, retries, backpressure. SNS Several independent consumers need the same message now, and the sender must not know who they are. EVENTBRIDGE Routing on content, schedules, other services’ events, or replay. Richer and slower — tens of milliseconds.
The standard production shape uses two of them together. Publish once to SNS or EventBridge, subscribe an SQS queue per consumer, and let each consumer keep its own backlog, its own retries and its own DLQ. A slow consumer then builds a queue instead of breaking the publisher, and a poisoned message lands in one team’s DLQ rather than stopping everyone.
SQSSNSEventBridge
Delivery modelPull. One consumer per message.Push. Every subscriber, in parallel.Push. Every matching rule’s targets.
OrderingNone (standard); per MessageGroupId (FIFO).None (standard); FIFO topics to FIFO queues.None. Do not rely on event order.
RetriesYours, driven by visibility timeout; DLQ after maxReceiveCount.Per subscription policy; a failing HTTPS endpoint just retries and gives up.24 hours of retries per target, then a DLQ if you configured one.
FilteringNone — consumers read everything on the queue.Filter policy on message attributes or body, evaluated at the topic.Event patterns over the whole JSON, including prefixes, ranges and negation.
LatencySingle-digit ms to receive; long polling waits up to 20 s for a message.Tens of milliseconds.Typically half a second — a router, not a hot path.
ReplayNo. Once deleted, it is gone.No.Yes: archive with a retention period, then replay a time window.
Message size256KB (extended client pattern for more).256KB.256KB.
CostsPer request, in 64KB chunks; the cheapest of the three at volume.Per publish plus per delivery.Per event published; targets billed separately.
Use it forWork: jobs, tasks, anything that must be done once.Notification: the same fact to several places, fast.Integration: routing, schedules, other services’ events.

Long polling, the 256KB ceiling, and redrive

Set ReceiveMessageWaitTimeSeconds to 20. Short polling returns empty responses against a sample of servers, which costs money and hides messages that are plainly there. For payloads over 256KB, do not compress harder — write the object to S3 and put the key in the message; the queue then carries a pointer and the consumer fetches the bytes it needs. And configure the DLQ with redrive from the start: a DLQ you cannot replay from is a folder of failures, not a recovery tool.

Cloud deep · 08

Lambda, API Gateway and Step Functions

Serverless is not “no servers”; it is a different set of limits. The three that decide your design are the execution environment’s lifecycle, the concurrency pool everything shares, and the fact that retry behaviour depends entirely on how the function was invoked.

ONE EXECUTION ENVIRONMENT — INIT ONCE, INVOKE MANY cold start INIT — runtime + imports INVOKE — handler 100 ms to 2 s, once per environment warm INVOKE — handler same process: globals, /tmp and pools survive Create SDK clients and connection pools outside the handler — they survive into the next invoke. Memory sets CPU: 1,769 MB is one full vCPU, so a larger, faster function is often the cheaper one. Hard cap: 15 minutes. ACCOUNT CONCURRENCY — ONE POOL, SHARED BY EVERY FUNCTION reserved: checkout 200 guaranteed, and capped reserved: export 100 cannot exceed 100 unreserved pool — 700 of a default 1,000 every other function competes here, and one runaway starves the rest Reserved concurrency carves up the pool. Provisioned concurrency keeps N environments initialised so there is no cold start — and bills for them whether or not a request arrives. Over the limit, the invoke is throttled and returns 429. RETRIES DEPEND ON HOW IT WAS INVOKED — THREE DIFFERENT RULES SYNCHRONOUS API Gateway, ALB, SDK invoke Lambda does not retry at all. The error goes back to the caller, who decides. Their timeout rules. ASYNCHRONOUS S3, SNS, EventBridge Two automatic retries, minutes apart. Then an on-failure destination or a DLQ — or silence. EVENT SOURCE MAPPING SQS, DynamoDB, Kinesis A poller batches records for you. SQS uses the queue’s own DLQ; a stream blocks until it succeeds. STEP FUNCTIONS — THE SAGA AS A STATE MACHINE YOU CAN SEE ReserveStock Retry: 3 × exponential ChargeCard Retry: 3 × exponential ShipOrder Retry: 2 × Succeed one execution, one id ReleaseStock compensating task FailOrder and say so to the caller Catch Retry and Catch are declarative — there is no orchestration code of your own to test, and every transition is recorded in the history.
The middle band is the one people get wrong. A function triggered by API Gateway that throws will not be retried by anything; a function triggered by S3 that throws will be retried twice and then vanish unless a destination is configured. Same code, same error, entirely different failure story — so decide where each invocation type’s failures land before you ship it.
Front doorWhat it is good atWhat it costs you
HTTP API (API Gateway v2)Lambda and HTTP proxying, JWT authorisers, CORS. Roughly a third the price of REST and lower latency.No request/response transformation, no API keys with usage plans, no WAF integration on its own.
REST API (API Gateway v1)Request validation and mapping templates, usage plans and API keys, WAF, private endpoints, canary stages.More expensive per million requests, more configuration surface, and mapping templates written in VTL.
Application Load BalancerCheapest at sustained high volume; the same front door for containers and Lambda targets.An hourly charge whether or not traffic arrives, no throttling per client, no request validation.
Lambda Function URLOne HTTPS endpoint on the function, IAM or no auth, zero extra service.No custom domain without CloudFront, no throttling, no authoriser beyond IAM. Fine for webhooks, not for a public API.
KnobWhat it doesWhat to actually do
MemorySets memory and, proportionally, CPU and network. 128MB to 10,240MB.Tune it: doubling memory often halves duration for no extra cost, and sometimes less. Measure, do not guess.
TimeoutUp to 900 seconds; the caller’s own timeout is usually shorter.Set it just above p99, not at the maximum. A 15-minute timeout on a 200 ms function turns a hang into a bill.
Reserved concurrencyGuarantees a slice of the account pool and caps the function at that number.Reserve for the function that must never be starved; cap the one that talks to a fragile database.
Provisioned concurrencyKeeps N environments initialised, so those invokes have no cold start.Only for user-facing p99 that a cold start would break. Combine with scheduled scaling; it bills by the hour.
Event source mappingA managed poller for SQS, DynamoDB Streams, Kinesis and Kafka, with batch size and window.Set functionResponseTypes: ReportBatchItemFailures so a single bad record does not replay the whole batch.
DestinationsOn-success and on-failure targets for asynchronous invokes.Always set an on-failure destination. Without it, an exhausted async retry is silently dropped.
Layers and container imagesShared dependencies, or a 10GB image instead of a 250MB zip.Prefer a small zip. Large artefacts lengthen init, which is exactly the number you are trying to reduce.
VPC attachmentPuts the ENI in your subnets so the function can reach RDS or ElastiCache.Only when you must. It removes public internet access unless you add a NAT gateway or VPC endpoints.

A VPC-attached Lambda in front of a relational database

Since the Hyperplane ENI change the per-invoke networking penalty is gone, so attachment is cheap — but the connection arithmetic is not. Each concurrent execution is its own process holding its own connection, so 400 concurrent invokes means 400 connections against an instance that allows perhaps 450, and there is no pool to share because there is no shared process. The fixes are all structural: put RDS Proxy in front, cap the function with reserved concurrency, or use a data store that speaks HTTP rather than sessions. Reaching for a bigger instance only moves the cliff.

Cloud deep · 09

CloudFront, and the rest of the working set

The last four services in the daily kit: the CDN that decides how much traffic your origin ever sees, the cache that sits in front of your database, the two places secrets live, and the registry your images come from.

ONE REQUEST THROUGH CLOUDFRONT viewer edge POP checks the cache key regional cache a second chance to hit origin: S3 via OAC bucket stays private miss miss hit — a few milliseconds, no origin request at all OAC signs the request with SigV4; the bucket policy allows only this one distribution. THE CACHE KEY IS WHATEVER YOU CHOOSE TO INCLUDE key = host + path + the query strings, headers and cookies you name Include Authorization and you cache per user, so the hit rate collapses. Include nothing and you can serve one user’s private page to everybody. invalidation vs versioned URLs An invalidation is a global purge: minutes to finish, rate-limited, and billed past 1,000 paths a month. A versioned name (app.9f2c1.js) never needs one — the new URL is simply a different object. The rest of the working set ELASTICACHE Cluster mode shards keys over 16,384 slots; the client must follow MOVED redirects. Failover promotes a replica in tens of seconds and every connection drops — so reconnect, and survive the miss storm that follows. SECRETS VS PARAMETERS SSM Parameter Store: free standard tier, 4KB, no rotation. Right for configuration and feature flags. Secrets Manager: about $0.40 per secret a month, built-in rotation, cross-account sharing. Pay for that. ECR AND IMAGES A private registry per region, with IAM instead of a password. Scan on push, and a pull-through cache for upstream images so a rate limit cannot stop a deploy. Deploy by digest: a moving tag is not a rollback.
Every CloudFront decision is really about the cache key. The distribution is easy to create and easy to render useless: forward every header and cookie and you have built an expensive proxy with a 2% hit rate. Start from the smallest key that is still correct, add one field at a time, and watch the hit-rate metric move.
ThingWhat it gives youWhat to actually do
Origins and behavioursOne distribution, several origins, routed by path pattern with its own cache policy each./static/* to the bucket with a long TTL, /api/* to the load balancer with caching disabled but compression on.
TTLsThe origin’s Cache-Control decides, bounded by the behaviour’s min, default and max TTL.Set Cache-Control at the origin and let CloudFront obey it. Overriding TTL in the console hides the real rule from everyone who reads the code.
Origin access controlCloudFront signs origin requests with SigV4, so the bucket can refuse everyone else.Use OAC, not the legacy OAI, and keep Block Public Access on. This is the correct way to serve a static site.
Signed URLs and cookiesTime-limited access to private content at the edge, with an optional IP restriction.Signed cookies for a whole path (a video library); signed URLs for one object. Not the same thing as an S3 presigned URL — these are checked at the POP.
InvalidationPurges paths from every POP.A break-glass tool. Version your asset filenames and you will rarely need it.
ElastiCacheManaged Redis or Valkey, sub-millisecond in-AZ, replicas and automatic failover.Set a TTL on every key, add jitter to it, and single-flight the origin fetch. A cache with no TTL policy becomes a second database with no backups.
Parameter StoreHierarchical config, standard tier free, versioned, KMS for SecureString.Everything non-secret, plus secrets you rotate manually. Cache values in the process — the throughput limit is real.
Secrets ManagerRotation via a Lambda, cross-account access, native RDS integration.Database credentials and third-party API keys. Pay the per-secret fee only where rotation actually earns it.
ECRPrivate image registry with IAM auth, lifecycle policies and image scanning.A lifecycle policy that keeps the last 20 tagged images and expires untagged ones after a day; otherwise storage grows forever.

The cache miss that takes the origin down

Both CloudFront and ElastiCache fail the same way. Everything is fine while the hit rate is high, so the origin is sized for the 5% that gets through — then a deploy changes the cache key, or a failover empties the cluster, and 100% of traffic arrives at a database that has never seen it. Size the origin for a plausible miss rate, not the happy one; add jitter so a thousand keys do not expire in the same second; and use a single-flight lock so one miss fetches from the origin while the rest wait for that result.

Part three

AWS foundations, in depth

Three things you configure once and live with for years: the address space, the permissions, and the traffic paths that generate the bill.

Cloud deep · 10

Address planning, and why EKS eats IPs

A VPC CIDR cannot be shrunk and awkwardly extended. On EKS the surprise is that pods take VPC addresses — so a subnet sized for instances runs out of IPs while the nodes still have free CPU.

VPC 10.0.0.0/16 — 65,536 ADDRESSES, ALLOCATED ONCE public · 3 × /24 private · 3 × /19 — nodes AND pods live here isolated · db spare Leave real spare space: a future EKS cluster, a peered VPC or a VPN cannot use an overlapping range. WHERE THE ADDRESSES ACTUALLY GO ON EKS one m5.large node 3 network interfaces × 10 addresses each = 29 pods maximum and 29 VPC IPs consumed the failure it causes Pods stick in ContainerCreating with “failed to assign an IP address”. Nodes look healthy, CPU is free, and autoscaling adds nodes that also cannot get addresses. what to do about it Size private subnets by pod count, not node count — /19 or larger per AZ. Or use a secondary CIDR for pods, or prefix delegation to pack more IPs per interface. Also reserved: AWS takes five addresses from every subnet, so a /28 gives you eleven usable, not sixteen.
This is the clearest example of a cloud detail leaking into application design. On EKS with the VPC CNI, a pod is a first-class network citizen — excellent for security groups per pod and for direct ALB targeting, and a hard capacity limit you must plan for on day one, when changing it is still cheap.
Cloud deep · 11

Where the money actually goes

Compute is the line item people optimise and rarely the surprise. The surprises are all data movement — charged per gigabyte, invisible in code review, and driven by architecture choices made months earlier.

EVERY ARROW HAS A PRICE pod A AZ-a pod B AZ-a FREE pod C AZ-b $0.01/GB — and again coming back NAT gateway $0.045/GB + hourly VPC endpoint S3/DDB: free internet ~$0.09/GB out Three bills that arrive as a surprise Cross-AZ chatter. Spreading pods over three zones is right for availability and doubles the charge on every chatty internal call. Topology-aware routing keeps traffic in-zone when a healthy local pod exists. NAT gateway processing. Pulling container images or writing to S3 through NAT is charged per gigabyte. A gateway endpoint for S3 costs nothing and removes that line entirely. Log volume. Ingestion is priced per gigabyte; debug logging left on in production can outspend the compute.
Cost is a design constraint you can reason about, not a finance problem you inherit. Each of these is a decision someone made in a diagram — where the pod sits, which path the bytes take, what gets logged — which means each is fixable in a diagram too.
Cloud deep · 12

How a pod gets AWS credentials

A pod has no access keys and should never have any. The chain that turns a Kubernetes service account into temporary AWS credentials is five steps, and knowing it turns "AccessDenied" from a mystery into a lookup.

1 ServiceAccount annotated: role-arn 2 projected token signed JWT, mounted 3 SDK calls STS WithWebIdentity 4 trust policy does this SA match? 5 temp creds 1h, auto-rotated No secret is ever stored, and the credentials expire on their own. The role is scoped to one service account in one namespace. WHEN IT SAYS ACCESSDENIED, CHECK IN THIS ORDER 1. Is the pod using the annotated service account at all? 2. Does the role’s sub condition name that exact namespace + SA? 3. Does the role’s permission policy allow the action and the resource ARN? 4. For S3 and KMS: does the resource policy also allow this principal? the anti-pattern this replaces One broad role on the node, inherited by every pod on it — so a compromised sidecar in any namespace gets whatever the busiest workload needed. Block the instance metadata endpoint from pods so this cannot be used by accident.
Every arrow here is checkable with one command. Read the service account on the pod, decode the projected token, read the role's trust policy, then its permission policy, then the bucket policy. Four lookups replace an afternoon of guessing — and CloudTrail records the call that was denied, with the principal it saw.
Part four

Data services, in depth

Managed databases move the operational work, not the physics. Two things stay yours: what the application experiences during a failover, and whether your key distributes.

Cloud deep · 13

What an RDS failover looks like from the application

"Multi-AZ" does not mean "no interruption". It means a standby is promoted automatically, and for roughly a minute your writes fail. What your service does in that minute is a design decision.

t = 0 → t = ~120s primary dies 0–30s detection writes hang, then error 30–60s promotion standby becomes primary 60–90s DNS flips endpoint → new address 90–120s reconnect pool rebuilds, writes work normal cold cache on the new box What the application must already do Cache DNS for seconds, not hours. A JVM or resolver caching the old address indefinitely turns a 2-minute event into a 2-hour one. Retry writes with backoff, and make them idempotent — some of those errors happened after the write committed. Serve reads from replicas or cache so the site degrades to read-only instead of going dark. Aurora shortens this window, it does not remove it.
Rehearse it. RDS lets you trigger a failover on demand — do it in staging under load and watch what your service actually does. Teams that have run this once have short, boring incidents; teams that have not discover their connection pool caches DNS forever, during a real one.
Cloud deep · 14

DynamoDB: the partition key is the whole design

Throughput is provisioned for the table and delivered per partition. A key that concentrates traffic gets throttled while the table as a whole looks half idle — the hot-key problem from Pillar A, with a hard published ceiling.

PK = order_date — ALL OF TODAY IN ONE PLACE 2026-08-19 THROTTLED yesterday older older The table is provisioned for 10,000 writes/s. One partition can take ~1,000. Every order today lands on the same one, so you get ProvisionedThroughputExceeded at 10% of what you are paying for. PK = customer_id — SPREAD BY DESIGN p1 p2 p3 p4 Even distribution uses the capacity you bought. The sort key (order_date#id) still gives you “this customer’s orders, newest first” as a single efficient query. For a genuinely hot key, add a shard suffix and fan out reads. The rule that follows: in DynamoDB you design the access patterns first and the schema second. There is no adding an index later to rescue a query you did not plan for — a GSI is a second copy of the table with its own key, its own cost, and its own write amplification.
This is the concrete version of "partitioning is the scaling move". The same key choice that removes coordination also concentrates load when chosen badly. Anything monotonic — a date, a sequence, a status flag — is the shape to avoid, in Dynamo, in Kafka, and in a sharded SQL cluster alike.
Part five

Kubernetes internals

Three mechanisms explain most cluster behaviour: what actually happens on apply, how the scheduler chooses a node, and how packets find a pod.

Cloud deep · 15

What happens when you apply a Deployment

Seven components touch your YAML before a container runs, and each is a separate place for it to stop. Knowing the sequence turns "the pod isn't starting" into a specific question with a specific command.

1 kubectl apply 2 API server authn, authz, admission 3 etcd — desired state 4 controllers Deploy → RS → Pods 5 scheduler binds pod → node 6 kubelet on node pulls image, mounts 7 CNI + runtime IP assigned, process up 8 readiness passes added to endpoints WHERE IT STOPPED → WHAT YOU SEE → WHAT TO RUN stuck at 2 admission webhook denied it the apply itself errors — read it stuck at 5 Pending — nothing fits kubectl describe pod → Events stuck at 6 ImagePullBackOff, missing Secret describe pod → Events, check the tag stuck at 7 ContainerCreating — no IP left subnet free addresses (section 02) stuck at 8 Running but 0/1 Ready kubectl logs — your probe is failing
"Running but not Ready" is the most informative state on this list. It means every piece of infrastructure worked and your own probe is refusing — so the answer is in your logs, not in the cluster. That single distinction saves most of the time usually lost to Kubernetes debugging.
Cloud deep · 16

How the scheduler picks a node

Two phases: filter removes nodes that cannot host the pod, score ranks the survivors. Every scheduling complaint — Pending pods, lopsided placement, all three replicas in one zone — is one of these two phases doing exactly what it was told.

12 NODES FILTER — CAN IT FIT AT ALL? SCORE — WHICH IS BEST? −4 not enough free requests −2 taint with no matching toleration −1 nodeSelector / affinity mismatch −1 no volume in that zone 4 survive + spreads across zones and hosts + balances utilisation + image already cached locally + affinity preferences honoured highest score wins → bind Three consequences worth knowing Filtering uses requests, not usage. A node at 20% real CPU can be “full” because its pods reserved everything. Over-requesting is how a cluster runs out of room while looking idle in every dashboard. Spreading is a preference unless you make it a rule. Use topologySpreadConstraints with DoNotSchedule if three replicas in one AZ is unacceptable — otherwise the scheduler may do it to satisfy something else. Scheduling happens once. Nothing moves a running pod when the balance changes; that needs a descheduler or a restart.
Every Pending pod is a filter that removed all twelve nodes. kubectl describe pod prints the count per reason — "3 Insufficient cpu, 2 node(s) had taint" — which is the answer, spelled out, in the place people least often look.
Part six

Networking, delivery and drift

How packets reach a pod, how new code reaches production, and how the thing you declared stops matching the thing that is running.

Cloud deep · 17

Pod networking, and the default that surprises people

Every pod gets a real routable address and can talk to every other pod in the cluster — across namespaces, by default, with nothing in the way. Kubernetes networking is open until you close it.

DEFAULT — FLAT AND OPEN ns: orders 10.0.42.7 ns: analytics 10.0.51.3 ns: payments 10.0.44.9 Analytics can reach payments. Nobody decided that. Namespaces are an organisational boundary and an RBAC boundary. They are not a network boundary. WITH A NETWORKPOLICY ns: orders app=orders ns: payments ingress: app=orders allowed :8443 ns: analytics dropped, silently The moment one policy selects a pod, everything not explicitly allowed to it is denied — including DNS, which is the classic first-policy outage. Allow egress to kube-dns on :53 in the same breath. Also: a NetworkPolicy needs a CNI that enforces it. With some plugins the object applies cleanly and does absolutely nothing — verify, don’t assume.
The mental model: security groups guard the VPC, NetworkPolicies guard inside the cluster. Both are allow-lists, and only one of them is on by default. A dropped packet from a policy produces a timeout with no log anywhere — so when a call that used to work starts hanging right after a platform change, this is the first thing to check.
Cloud deep · 18

Three ways to replace running code

Rolling, blue/green and canary differ in one variable: how much traffic sees the new version before you find out whether it works. That choice buys either cost efficiency or a smaller blast radius.

ROLLING — THE DEFAULT new version share: 25 → 50 → 75 → 100% No extra cost, no extra infrastructure. Both versions serve at once, so the database must accept both — and rollback takes another full roll. BLUE / GREEN — ATOMIC SWITCH 0% → 100% in one DNS or target-group flip Rollback is the same flip back, in seconds — the real reason to choose it. Costs double capacity during the overlap, and everyone meets the bug at the same instant. CANARY — SMALLEST BLAST RADIUS 1% → 5% → 25% → 100%, each step gated on metrics Only a canary actually tests the release: automatic rollback on error rate or latency, before most users are affected. Needs traffic splitting and metrics good enough to trust. The schema comes first None of these work if the new code needs a schema the old code cannot read. Expand, migrate, contract — three deploys, never one.
Pick by what rollback costs you. If reverting means another ten-minute roll while errors accumulate, buy blue/green. If a bad release must never reach more than a fraction of users, buy canary and the metrics to gate it. Rolling is the right default precisely when neither of those is worth the money.
Cloud deep · 19

Infrastructure as code, and drift

The value of Terraform is not automation — it is that the intended state is reviewable and diffable. That value evaporates the first time someone fixes production in the console and does not tell the code.

.tf in git what we intend state file what we last made real AWS what exists now plan apply someone in the console “just this once, it’s urgent” DRIFT What the next plan does with that drift Silently reverts it. The urgent fix disappears during an unrelated deploy — the same incident, twice, with no obvious cause. Or fails to apply, blocking every other change until someone reconciles by hand under time pressure. The practice that prevents both: plan on every pull request, apply only from CI, run a scheduled drift check, and give console access read-only — with a documented break-glass path that files an issue when it is used.
The state file is the part people underestimate. It is the only record connecting code to real resources, it contains secrets in plaintext, and losing it means reimporting your infrastructure by hand. Remote backend, encrypted, versioned, with locking — on day one, not after the first corruption.
Part seven

Operating it

The last mile: knowing what to measure, what deserves to wake someone, and where to look first when it does.

Cloud deep · 20

What to measure, and what deserves a page

Two standard methods: RED for things that serve requests, USE for things that are resources. Together they cover a system without a hundred bespoke dashboards nobody reads.

RED — for services Rate — requests per second Errors — as a proportion Duration — p50, p95, p99 never the mean alone USE — for resources Utilisation — how busy Saturation — queue depth Errors — throttles, drops pools, disks, partitions WHAT ACTUALLY PAGES A HUMAN Only symptoms a user would notice, tied to an SLO: error rate > budget burn · p99 > SLO · queue age rising CPU at 90% is not an incident. A slow checkout is. causes belong on dashboards; symptoms belong on pagers Three signals, three jobs METRICS — is something wrong? Cheap, aggregated, alertable. Keep label cardinality bounded — never a user ID. TRACES — where is it wrong? One request across every hop of section 01. Sample the slow ones, not 1 in 1000. LOGS — what exactly happened? Structured, with the trace ID, so one click connects all three.
The connective tissue is the trace ID. An alert fires on a metric, the trace shows which hop is slow, the logs for that trace say why. Without the shared ID you have three separate tools and a guess — which is why propagating the ID through every service and into every log line is worth more than any individual dashboard.
Cloud deep · 21

Symptom → first check

A lookup table for the ten things that actually happen. Each row is a symptom you will see on a dashboard, the check that discriminates fastest, and the cause it usually turns out to be.

SymptomCheck firstUsual cause
502 from the load balancerTarget group health, readiness probesAll pods failing readiness — often a dependency the probe touches.
504 from the load balancerYour p99 vs the LB idle timeoutA handler with no deadline, waiting on something slow.
Pods Pendingdescribe pod → EventsRequests do not fit, a taint, or no node in the required zone.
ContainerCreating foreverFree IPs in the subnet; Secret existsCNI address exhaustion, or a missing volume/secret.
CrashLoopBackOfflogs --previousConfig or migration failure at start-up — not Kubernetes.
Latency up, CPU flatCPU throttling metric, pool waitsA CPU limit throttling you, or a saturated connection pool.
OOMKilled, exit 137Memory limit vs actual working setLimit set from a guess, or a genuine leak — take a heap profile.
Intermittent timeouts to one serviceNetworkPolicy, security group, DNSA policy dropping packets silently, or DNS throttling under load.
AccessDenied from the SDKCloudTrail for the exact denied callTrust policy, permission policy, or a resource policy — in that order.
Bill up, traffic flatCost Explorer by usage typeNAT processing, cross-AZ transfer, or log ingestion.
Cloud deep · 22

What “hands-on” actually means

The phrase in the job description is doing a lot of work. Here is a concrete reading of it — things you have done, not things you have configured once from a tutorial.

  1. You have drawn your own VPC from memory — which subnets are public, what has a route to the internet gateway, and which security group references which.
  2. You have debugged an AccessDenied to its actual cause using CloudTrail, and can say whether it was a trust policy, a permission policy or a resource policy.
  3. You have watched a failover — deliberately triggered or otherwise — and know what your application did during it.
  4. You have found a Pending pod’s reason in the events rather than by restarting things until it moved.
  5. You have set requests and limits from real measurements, and can explain why one of your workloads has no CPU limit.
  6. You have shipped a change through a canary or blue/green, and rolled one back when the metrics said to.
  7. You have found a cost line you did not expect and removed it with an architectural change rather than a smaller instance.
  8. You have written the Terraform, reviewed someone else’s plan output, and cleaned up drift that a console change created.

The through-line across all three pillars

The foundations page said every system is queues connected by contracts. The Go page said every goroutine needs an owner and an exit. This page says every hop needs an identity, a bound and a bill. They are the same discipline at three altitudes — and the reason the job description asks for all three is that an incident never stays in one of them.