Route 53, CloudFront & ELB

DNS routing, CDN, and load balancing.

Intermediate30 min · lesson 8 of 15

Order a pizza from a global chain and three things happen before the box reaches you. Something tells you which branch is nearest, and that job belongs to Route 53. Each branch keeps its bestsellers warm under the counter rather than cooking every order in one central kitchen, which is what CloudFront does. Inside the branch, a floor manager waves each customer toward the register with the shortest queue and steers everyone away from the one with a broken card reader. That is Elastic Load Balancing (ELB). AWS's traffic layer is the same trio: find the address, serve from somewhere close, hand the work to whoever can take it. Every one of those decisions is made on measured health, not hope.

Three words before any commands. DNS (Domain Name System) is the internet's phone directory. It turns a name like app.example.com into the numeric IP addresses machines actually dial. A CDN (content delivery network) keeps copies of your content on servers physically close to your users, so the bytes travel a shorter road. A load balancer is one stable front door that spreads arriving requests across many identical backends. Route 53 is AWS's DNS, CloudFront is its CDN, and ELB is its family of load balancers. All three are managed for you, so your work is describing behavior rather than running servers.

Route 53: a directory that makes decisions

A hosted zone is the folder Route 53 keeps one domain's records in, and it costs $0.50 a month. Inside it, an A record maps a name to IPv4 addresses. A CNAME (canonical name) record maps one name to another name. Route 53 adds an extension the DNS standard never had: the alias record, which points straight at an AWS resource such as a load balancer or a CloudFront distribution. Two things make aliases worth remembering for the exam. They work at the zone apex (example.com with nothing in front of it), where the DNS spec flatly forbids a CNAME. And when the answer resolves to an AWS resource, the query is free.

Routing policies are what turn a directory into an architecture tool. *Simple* hands back the record exactly as written. *Weighted* splits traffic by ratio, which is how you run a canary release: send 5% of visitors to the new stack and watch what breaks. *Latency-based* answers with whichever Region is fastest for that user by measured network latency, which is not always the closest one on a map. *Geolocation* answers based on where the user is, handy for data-residency rules or a localized site. *Failover* keeps serving your primary while its health check passes, then swings to a secondary when it stops passing. A health check here is a small fleet of Route 53 probers hitting your endpoint from several places in the world and voting. *Multivalue answer* returns up to eight healthy records at once so a client that gets a dud can retry another. Here is failover, the disaster-recovery workhorse, end to end:

route53-failover.sh
# 1. Health check that probes the primary endpoint every 30s
aws route53 create-health-check \
--caller-reference app-primary-2026-07-13 \
--health-check-config '{
"Type": "HTTPS",
"FullyQualifiedDomainName": "primary.example.com",
"ResourcePath": "/healthz",
"RequestInterval": 30,
"FailureThreshold": 3
}'
# { "HealthCheck": { "Id": "9f2b3c44-8a1e-4c5d-b7f0-2d6e8a9c1b3d", ... } }
# 2. PRIMARY failover record: an alias to the ALB, gated by that check
aws route53 change-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEFGHIJ \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "app.example.com",
"Type": "A",
"SetIdentifier": "primary",
"Failover": "PRIMARY",
"HealthCheckId": "9f2b3c44-8a1e-4c5d-b7f0-2d6e8a9c1b3d",
"AliasTarget": {
"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "web-alb-1234567890.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
}]
}'
# {
# "ChangeInfo": {
# "Id": "/change/C2682N5HXP0BZ4",
# "Status": "PENDING",
# "SubmittedAt": "2026-07-13T09:14:22.513000+00:00"
# }
# }
# 3. Ask Route 53's own servers what resolvers will see
aws route53 test-dns-answer \
--hosted-zone-id Z0123456789ABCDEFGHIJ \
--record-name app.example.com --record-type A
# {
# "Nameserver": "ns-2048.awsdns-64.com",
# "RecordName": "app.example.com",
# "RecordType": "A",
# "RecordData": ["203.0.113.24", "203.0.113.57"],
# "ResponseCode": "NOERROR",
# "Protocol": "UDP"
# }

The change lands as PENDING and turns INSYNC across Route 53's whole fleet in roughly 60 seconds. Setting EvaluateTargetHealth: true adds a second safety net, because the alias then inherits the load balancer's own opinion of its targets. One exam fact worth carrying in: Route 53 is the only AWS service with a 100% availability SLA (service level agreement). The management API can have a bad day. Resolution keeps answering.

ELB: three load balancers, three different jobs

ELB is a family name, and the family has three members. The Application Load Balancer (ALB) works at Layer 7, the HTTP/HTTPS layer, which means it opens each request and reads it. That lets it route on URL path, hostname, header, or query string, sending each kind of request to a different target group, a named set of backends (EC2 instances, raw IP addresses, containers, or Lambda functions). The Network Load Balancer (NLB) works at Layer 4, the raw TCP/UDP layer, where TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) are the two ways packets get shipped. It never parses HTTP at all, which is exactly why it pushes millions of requests per second at very low latency and, alone in the family, offers a static IP address per Availability Zone, or lets you bring your own Elastic IP. The Gateway Load Balancer (GWLB) is the specialist of the three: it invisibly funnels traffic through a fleet of third-party security appliances using the GENEVE protocol. Exam keywords map cleanly: "route by URL path" → ALB; "static IP" or "extreme performance" → NLB; "inline firewall fleet" → GWLB.

Under the hood every balancer runs the same small loop. Probe each target on a timer. Mark it healthy after N passes in a row and unhealthy after N failures in a row. Send traffic only to the healthy ones. A listener is the balancer's front desk: it declares what the balancer accepts (a protocol and a port) and where it forwards. TLS (Transport Layer Security, the padlock in the browser bar) ends at the listener, using a free certificate from ACM (AWS Certificate Manager), so your backends can speak plain HTTP inside the VPC (Virtual Private Cloud). Build the pieces:

alb-setup.sh
# Target group: WHERE traffic goes and HOW "healthy" is decided
aws elbv2 create-target-group \
--name web-tg --protocol HTTP --port 80 \
--vpc-id vpc-0a1b2c3d4e5f67890 --target-type instance \
--health-check-path /healthz \
--health-check-interval-seconds 15 \
--healthy-threshold-count 2 --unhealthy-threshold-count 3 \
--matcher HttpCode=200-299
# { "TargetGroups": [ { "TargetGroupArn":
# "arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067", ... } ] }
aws elbv2 register-targets \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067 \
--targets Id=i-0a1b2c3d4e5f67890 Id=i-0f9e8d7c6b5a43210
# HTTPS listener: TLS ends here, with an ACM cert from the SAME Region
aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:loadbalancer/app/web-alb/50dc6c495c0c9188 \
--protocol HTTPS --port 443 \
--certificates CertificateArn=arn:aws:acm:us-east-1:111122223333:certificate/12345678-1234-1234-1234-123456789012 \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067
# The command you will run most often in production:
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/web-tg/73e2d6bc24d8a067
# {
# "TargetHealthDescriptions": [
# { "Target": { "Id": "i-0a1b2c3d4e5f67890", "Port": 80 },
# "TargetHealth": { "State": "healthy" } },
# { "Target": { "Id": "i-0f9e8d7c6b5a43210", "Port": 80 },
# "TargetHealth": { "State": "unhealthy",
# "Reason": "Target.ResponseCodeMismatch",
# "Description": "Health checks failed with these codes: [503]" } }
# ]
# }

That Target.ResponseCodeMismatch reason is where debugging starts, because the ALB tells you exactly what the target said back. One knob to know by name: deregistration delay, 300 seconds by default, the grace period a removed target gets to finish the requests already in flight before the balancer cuts it off. Set it long and deployments crawl. Set it short and you chop slow requests in half.

All targets unhealthy? Check the security group before you blame the app
When every target in a group fails its health checks, an ALB fails open. It routes to all of them anyway, so users see whatever the targets themselves return: their own error pages, 502s, or 504s. (A clean 503 generated by the ALB itself means something different, that the target group has no registered targets at all.) The two usual causes of an all-unhealthy group live outside your code. First, the target's security group has to allow inbound traffic *from the ALB's security group* on the health-check port. If it only allows your office IP address, every probe times out and the whole group drains. Second, an app that answers HTTP with a 301 redirect to HTTPS will fail a check expecting 200. Point the probe at a path that does not redirect, such as /healthz, or widen --matcher to HttpCode=200-399. Either way, describe-target-health names the exact failure for you.

CloudFront: your app, copied to 750+ doorsteps

CloudFront sits between your users and your origin, the one real source of the content, usually an S3 bucket or an ALB. A request lands at the nearest edge location, and AWS runs more than 750 of these points of presence worldwide. If that edge already holds the file (a cache hit), it answers in single-digit milliseconds and your origin never hears about the request. On a miss, the request falls back to one of 15 larger regional edge caches, then to the origin, and the response is stored on the way back out for as long as its TTL (time to live, the shelf life of a cached copy) allows. CloudFront reads your Cache-Control headers and falls back to 24 hours when you say nothing. Cache behaviors let a single distribution treat paths differently, sending /api/* straight through to an ALB uncached while /assets/* caches hard from S3.

Three things to get right in production. *Security*: lock an S3 origin behind Origin Access Control (OAC) so objects can be read only through the distribution and never by hitting the bucket URL. OAC replaced the older OAI (Origin Access Identity) and, unlike OAI, works with objects encrypted by SSE-KMS. Attach AWS WAF (Web Application Firewall) at the distribution, and remember the trap the exam loves: an ACM certificate used by CloudFront must live in us-east-1, whichever Region the origin runs in. *Cost*: moving data from an AWS origin into CloudFront is free, and edge egress rates undercut Region egress rates, so putting CloudFront in front of even a fully dynamic ALB often shrinks the bill. *Freshness*: when you truly have to purge something early, invalidate it:

cloudfront-ops.sh
# What's deployed right now?
aws cloudfront list-distributions \
--query "DistributionList.Items[].{id:Id, domain:DomainName, status:Status, origin:Origins.Items[0].DomainName}" \
--output table
# ----------------------------------------------------------------------------------------------------------
# | ListDistributions |
# +--------------------------------+------------------+-------------------------------------+--------------+
# | domain | id | origin | status |
# +--------------------------------+------------------+-------------------------------------+--------------+
# | d111111abcdef8.cloudfront.net | E2QWRUHAPVYC32 | assets.s3.us-east-1.amazonaws.com | Deployed |
# +--------------------------------+------------------+-------------------------------------+--------------+
# Purge stale objects after a deploy (a wildcard counts as ONE path)
aws cloudfront create-invalidation \
--distribution-id E2QWRUHAPVYC32 \
--paths "/index.html" "/assets/*"
# {
# "Location": "https://cloudfront.amazonaws.com/2020-05-31/distribution/E2QWRUHAPVYC32/invalidation/I3AB0245EXAMPLE",
# "Invalidation": {
# "Id": "I3AB0245EXAMPLE",
# "Status": "InProgress",
# "CreateTime": "2026-07-13T09:31:07.815000+00:00"
# }
# }
# Block until every edge has dropped the old copies (typically 1-5 min)
aws cloudfront wait invalidation-completed \
--distribution-id E2QWRUHAPVYC32 --id I3AB0245EXAMPLE

The first 1,000 invalidation paths each month are free; every path after that costs $0.005. The better habit is to almost never invalidate at all. Give assets versioned filenames like app.3f2a1c.js with year-long TTLs, and keep only the entry points such as /index.html on a short leash.

The whole path, front to back

Chained together, the three services give you availability in layers, because every hop checks the health of the next one before handing anything over:

One request, three health-checked hops
1User
resolves app.example.com
2Route 53
alias answer · failover policy
3CloudFront edge
cache hit? serve · miss? forward
4ALB listener
TLS ends · L7 rule picks target group
5Healthy targets
multi-AZ EC2, probed every 15 s
Route 53 probes your endpoint, CloudFront retries a failed origin connection, and the ALB forwards only to targets that pass their checks. Failures get routed around instead of shown to users.

Trade-offs and exam triggers

A few trade-offs are worth burning into memory. DNS failover is capped by TTL, because clients cache the answer they were given, so a Route 53 failover takes seconds to minutes and never milliseconds. An alias record pointing at an ELB, for instance, uses a fixed 60-second TTL you cannot change. Cross-zone load balancing, which lets a balancer node in one Availability Zone send traffic to targets in the other zones rather than only its own, is on by default and free on ALB, but off by default on NLB, where switching it on adds inter-AZ data charges. ALBs warm up gradually behind the scenes; NLBs are built to absorb a spike that arrives all at once. And CloudFront caches *dynamic* content perfectly well: even a one-second TTL flattens a thundering herd into a single origin request per edge.

You can now steer a request to your application, serve it from somewhere close, and land it on a target that is actually alive. Almost every request that survives the cache ends the same way, though: reading or writing state. Next comes the data tier, RDS, Aurora & DynamoDB, where the questions turn into durability, replication, and picking relational or key-value at scale.

Route 53 answers names, and it also decides where each user gets sent. Latency, failover, geolocation, and weighted policies move traffic on health and location. Alias records pointing at ALBs and CloudFront distributions keep you from hard-coding raw IP addresses that change under your feet.

ALB is Layer 7 and understands HTTP hostnames and paths. NLB is Layer 4 and moves raw TCP and UDP at extreme volume. Classic Load Balancer is the legacy option you inherit, never the one you pick. Health checks decide whether a target sees traffic at all, and unhealthy targets are where "the load balancer is broken" usually turns out to live.

CloudFront goes in front for anything cacheable, and it terminates TLS at the edge. For an S3 origin, Origin Access Control beats a public bucket every time. Invalidations are a repair tool, never a deploy strategy: version your object keys instead.

Try this

List your hosted zones and a quick distribution summary, then check one load balancer's scheme and DNS name. Everything here is read-only, so pointing it at lab resources is safe.

terminal
aws route53 list-hosted-zones --query 'HostedZones[].{Name:Name,Id:Id,Private:Config.PrivateZone}' --output table
aws elbv2 describe-load-balancers --query 'LoadBalancers[].{Name:LoadBalancerName,DNS:DNSName,Scheme:Scheme,Type:Type}' --output table
aws cloudfront list-distributions --query 'DistributionList.Items[].{Id:Id,Domain:DomainName,Status:Status}' --output table
output
-----------------------------------------------
| ListHostedZones |
+-------------------+-------------+-----------+
| Name | Id | Private |
+-------------------+-------------+-----------+
| example.com. | /hosted... | False |
+-------------------+-------------+-----------+
app-alb | app-alb-123.elb.amazonaws.com | internet-facing | application
E123ABC | d111111abcdef8.cloudfront.net | Deployed

Takeaway

Rule of thumb: DNS picks where a user gets sent, the load balancer spreads work across targets that are alive, and CloudFront shortens the trip for bytes worth caching. Health checks are the thread running through all three.

Next, sketch the path yourself: browser → Route 53 → CloudFront → ALB → private targets. Mark which hop fails closed when a health check goes red.

Quick check
01Every target in an ALB target group has gone unhealthy and your users are getting 5xx errors. Two causes account for most cases like this, and neither one is in your application code. Which pair?
Correct — Both live in the infrastructure rather than the code. The target's security group has to accept inbound traffic from the ALB's security group on the probe port, and a check landing on a path that redirects to HTTPS will never get the 200 it wants unless you probe an unredirected path or widen the matcher.
Incorrect — No. An empty target group gives you a clean ALB-generated 503, which is a different symptom, and cross-zone balancing is on by default for an ALB. Neither one drains a group that already has targets.
Incorrect — No. The us-east-1 certificate rule is a CloudFront gotcha, and TTL controls how stale a cached copy gets. Neither makes a target fail its health check.
Incorrect — No. Deregistration delay only governs how long a removed target has to finish in-flight requests, and latency-based routing belongs to Route 53. Neither one turns a whole target group unhealthy.
02Your application sits behind an Application Load Balancer, and you need the zone apex (example.com, with nothing in front of it) to point at it. Why does that require a Route 53 alias record instead of a CNAME record?
Incorrect — No. Health-check compatibility is not the constraint here; the apex restriction comes from the DNS specification itself.
Correct — Aliases work at the zone apex where the DNS spec bans CNAMEs, and a query that resolves to an AWS resource costs nothing.
Incorrect — No. An alias to an ELB does use a fixed 60-second TTL, but TTL is not the reason the apex needs an alias.
Incorrect — No. A CNAME can point at any hostname, an AWS DNS name included. The real limit is that CNAMEs are illegal at the apex.
03A multiplayer game backend has to accept millions of TCP (Transmission Control Protocol) connections per second at the lowest latency you can get, and every game server must sit at a fixed, unchanging IP address that players' clients already have configured. Which load balancer fits BEST?
Incorrect — No. An ALB opens and parses HTTP at Layer 7 and offers no static IP, so it is the wrong tool for raw high-volume TCP traffic.
Incorrect — No. A GWLB exists to push traffic through third-party security appliances over GENEVE, not to terminate client-facing game connections.
Correct — "Static IP" and "extreme performance" both point at the NLB, which works at Layer 4 and hands you a static IP per Availability Zone.
Incorrect — No. The NLB is the one with static IPs, and the Classic Load Balancer is legacy, never the low-latency, high-throughput answer.

Related