Lesson 12 · 25 min read

ALB and Auto Scaling: Scaling to 1000+ Concurrent Users

One server becomes many. Health checks, target groups, and servers that appear when traffic spikes and vanish when it drops.

What you'll learn in this lesson

  1. Concept: many servers, one address
  2. Why one server is not enough
  3. The prerequisite: stateless servers
  4. What does “1000 concurrent users” actually mean
  5. How an ALB works: listener, rule, target group, health check
  6. How Auto Scaling works: template, group, policy
  7. Build it, step by step
  8. Health checks done properly
  9. Choosing a scaling policy
  10. Deploying without downtime
  11. Prove it: load test and kill a server
  12. The common pitfalls that cause real 502s
  13. What this costs
  14. Troubleshooting table
  15. Interview corner
  16. Practice task before Lesson 13
  17. Conclusion

Concept

Two AWS services, one job: make your app survive success.

  • An Application Load Balancer (ALB) is an AWS service that gives users one stable address. It shares their requests across many identical servers. It sends traffic only to servers that are healthy.
  • Auto Scaling is an AWS service that decides how many servers exist. It starts more servers when traffic rises. It replaces servers that fail. It removes extra servers when the busy time is over.
The analogy — a restaurant on a busy night

The ALB is the host at the door. The host greets everyone and sends each group to a free table. The host never seats anyone at a table with a broken leg (that is the health check). Auto Scaling is the manager. The manager calls in more waiters when the queue grows and sends them home when it is quiet. The customers only ever see one restaurant.

1,000 users → yourapp.com
ALB
one public address · health check GET /health every 30 s
EC2 #1
healthy
EC2 #2
healthy
EC2 #3
failed check — no traffic
EC2 #4
launching (CPU > 70%)
Auto Scaling group · min 2 · desired 3 → 4 · max 10 · rule: add 1 when avg CPU > 70% for 5 min
ALB + Auto Scaling. The load balancer only sends traffic to servers that pass its health check; Auto Scaling changes how many servers exist based on load. Users see one address the whole time.

Why one server is not enough

  • A limit on capacity. A t3.small has 2 vCPUs (virtual CPU cores). After a certain point, extra requests only wait in line, slow down, and then time out.
  • A single point of failure. This means one part that stops everything when it breaks. With one server, one crash, one bad deploy or one reboot for updates takes the whole site down.
  • Deploys cause downtime. With one server, users have nowhere to go while you restart it.
  • You pay for the peak all month. A server big enough for the busiest hour sits idle for the other 23 hours. Elastic capacity (capacity that grows and shrinks) charges you only for what you use.

Now the comparison between AWS and Vercel from Lesson 0 becomes real. Vercel scaled your app for you, out of sight. You are now building the same behaviour yourself. You can see every part and change every part.

The prerequisite: stateless servers

Load balancing has one strict rule: any request may go to any server. A stateless server keeps no data of its own between requests. If a server remembers something that the other servers do not, users get random bugs that are hard to find. Lessons 8 and 11 removed the two biggest causes (the database and the files). Check the rest before you scale:

Stateless · Vercel
Request 1→
instance Acounter = 1
Request 2→
instance Bcounter = 1
Request 3→
instance Ccounter = 1

Each instance disappears after answering. Nothing carries over.

Persistent · EC2 / Railway
Request 1Request 2Request 3
→→→
one process, 24/7counter = 1
counter = 2
counter = 3

Same process every time, so memory survives between requests.

Three requests, same counter code. Stateless: each request starts from a fresh instance. Persistent: one process remembers.
If your app does this……scaling breaks like thisFix
User sessions in server memoryServer B does not know that the user logged in on server ASigned cookie or JWT (a JSON Web Token: a small signed piece of text that holds the login proof, kept in the browser), or Redis (Lesson 16)
Uploaded files on local diskOnly one server has the fileS3 (Lesson 11)
Data in a local database or SQLite fileEach server has different dataRDS (Lesson 8)
In-memory cache or countersEach server has its own copy, so answers differRedis (a fast data store that many servers can share), or accept a separate cache on each server for data that is not critical
Cron jobs / setInterval in the app (a cron job is a task that runs on a timer, for example every night)With N servers, the job runs N timesUse one scheduler (EventBridge is an AWS service that starts tasks on a schedule), or a single worker. Never run it in the web servers
Settings or secrets in a file that you edit by handNew servers start without that fileSSM Parameter Store or Secrets Manager (AWS services that keep settings and secrets safe), read when the server boots
Sticky assumptions like “the same server next time”The load balancer may pick any serverMake every request work on any server
Scaling a stateful app multiplies bugs, not capacity

The most common failure after “we added a second server” is that users are logged out on every other click. Do this check first. It is much cheaper than debugging in production.

What does “1000 concurrent users” actually mean

The lesson title promises 1000+ concurrent users. Before you buy anything, learn what that number means. “Concurrent users” are people who use the site at the same time. A request is one call from a browser to your server. Concurrent users are not requests per second. A person spends most of the time reading, not clicking.

The back-of-envelope formula

requests per second ≈ concurrent users ÷ seconds between actions. If 1,000 users each do something every 10 seconds, that is about 100 requests per second. If each does something every 3 seconds, it is about 330 requests per second. One page load is several requests (HTML, API calls, files). But with a CDN, the files never reach your servers.

Next, ask how many requests one server can handle:

Kind of requestRough throughput per vCPU
Tiny JSON response, no database500–2,000 req/s
Server-rendered page, a couple of database queries30–150 req/s
Heavy: image processing, reports, big joins1–10 req/s

These ranges are wide on purpose. You must measure your own app (see the load-test section). Say your app does about 60 requests per second per vCPU. Then a t3.small (2 vCPUs) handles about 120 requests per second. 1,000 casual users send about 100 requests per second. That is close to the limit of one server. You also need extra room for spikes and for the loss of one server, so you need 2–3 servers. That is why the group has a minimum of 2 (one in each Availability Zone, or AZ: a separate group of data centres inside a region) and a maximum of 6.

One more warning about t3 servers. They are burstable. They can use more than their normal CPU share only while they have CPU credits. Under steady heavy load the credits run out. Then the server slows down, or (in the default “unlimited” mode) AWS charges extra. For steady heavy load, test a non-burstable type such as m7i.

The database usually breaks first

A connection pool is a set of ready database connections that a server reuses. Six servers with a pool of 10 each use 60 database connections. A small RDS instance allows about 100 (Lesson 8). When you scale the web servers, work out the connection count at the maximum number of servers. You can also use RDS Proxy, an AWS service that shares database connections for you.

How an ALB works: listener, rule, target group, health check

PartWhat it isOurs
Load balancerThe public entry point. It lives in at least two public subnets (parts of your network that the internet can reach) in two AZs. It gets a DNS name, not a fixed IP.myapp-alb
Listener“Listen on this port and protocol.” Holds the rules and the TLS certificate.443 HTTPS, 80 HTTP → redirect
Rule“If host/path/header matches, do this action” (forward, redirect, fixed response).default: forward to myapp-tg
Target groupThe group of servers that share the work, plus the rules for checking their health.myapp-tg, port 80
Health checkThe ALB asks for a path every few seconds. Only servers that answer 200 (“OK”) receive traffic.GET /api/health

Rules let one ALB serve many apps. A host-based rule looks at the domain name: api.yourapp.com goes to the API target group, and yourapp.com goes to the web target group. A path-based rule looks at the URL path: /api/* goes to one group and everything else goes to another. This way, one load balancer (about $20 a month) does the work of several.

Which load balancer?

AWS has four types. The “layer” is a level in the network model. A layer 7 balancer can read web requests. A layer 4 balancer only sees connections (TCP or UDP).

TypeWorks atUse for
ALB (Application)Layer 7 (the application layer): understands HTTP. Routes by host name, path or header. Also handles TLS, WebSockets and redirectsWeb apps and APIs — our choice
NLB (Network)Layer 4 (the transport layer): passes raw TCP/UDP traffic (TCP and UDP are the two basic ways to send data over a network). Handles millions of connections, gives fixed IPs, and adds very little delayDatabases, games, protocols that are not HTTP, or when you need a fixed IP
Gateway LBLayer 3 (the network layer): sends traffic through firewall or inspection appliancesSecurity appliances (rare)
Classic LBThe old load balancer that came before ALB and NLBDo not use for new work
TLS moves to the ALB

The ALB ends the HTTPS connection (TLS is the protocol that encrypts HTTPS traffic). It uses a free ACM certificate that AWS renews for you. So the servers behind it no longer need Certbot or port 443. They speak plain HTTP on port 80 inside the VPC. This is the reward for learning about ACM in Lesson 4.

How Auto Scaling works: template, group, policy

There are three parts. Each one builds on the one before:

  1. Launch template is a saved recipe for one server. It holds the AMI (a saved copy of a server disk with the operating system), the instance type (the size), the key, the Security Group, the IAM profile (the permissions the server gets), and a user-data script that runs on the first boot. It has versions, so you can change it and go back to an older one.
  2. Auto Scaling Group (ASG) is a group that follows this rule: “keep between min and max servers from this template, spread across these subnets, and registered in this target group.” It replaces failed servers automatically.
  3. Scaling policy is the rule that moves the wanted number of servers between min and max. For example: “keep the average CPU near 50%”.
Pets vs cattle

The server in Lesson 7 was a pet. It had a name, you cared for it by hand, and you could not replace it. ASG servers are cattle. They are all the same, they have numbers, and when one is sick you replace it instead of repairing it. The user-data script makes the replacement fast. A new server must get fully ready with zero human steps. This is the reason Lessons 9 and 11 came first.

Build it, step by step

You need the network from Lesson 6, an image in ECR (the AWS store for Docker images, Lesson 9) and a domain (Lesson 3). Each step is one console screen. We only mention the fields that matter.

  1. Request the HTTPS certificate

    Open ACM (AWS Certificate Manager) in ap-south-1, the ALB's region. Request a public certificate for yourapp.com and *.yourapp.com. Choose DNS validation. This means you prove that you own the domain by adding a DNS record. Click “Create records in Route 53”, and the certificate is ready in a few minutes. The wildcard covers every tenant subdomain. The bare domain is listed on its own, because *.yourapp.com does not match yourapp.com itself.

  2. Create the target group (the pool)

    Go to EC2 → Target groups → Create. Choose type Instances, HTTP port 80, and the myapp VPC. Set the health check path to /api/health, the interval to 15 s, healthy after 2 passes, and unhealthy after 3 failures. Do not register any targets. Auto Scaling will do that. Then, under Attributes, lower the deregistration delay from 300 to 30 seconds. This is how long the ALB waits for requests that are still running on a server before it removes the server.

  3. Create the load balancer and its listeners

    Go to EC2 → Load balancers → Application Load Balancer. Choose internet-facing, the two public subnets, and the security group alb-sg. Add two listeners:

    • HTTPS 443: forward to myapp-tg, with the ACM certificate. The encryption ends here.
    • HTTP 80: redirect to HTTPS 443 (a 301 “moved permanently” answer).

    alb-sg (Lesson 6) already accepts ports 80 and 443 from everyone. web-sg accepts port 80 only from alb-sg. So the public can no longer reach a server directly. Remove the temporary 0.0.0.0/0 rules for ports 80 and 443 on web-sg that Lesson 10 added.

  4. Store the deploy settings where new servers can read them

    A server that Auto Scaling starts at 3 a.m. has no human to tell it which image to run. It reads this from SSM Parameter Store. This is an AWS service that stores settings and secrets as named values. It is free for standard parameters. Create two:

    • /myapp/image-tag is a plain String. It holds the commit SHA (the short ID of a Git commit) to run, for example a1b2c3d.
    • /myapp/env is a SecureString (it is stored encrypted). It holds the whole production env file, including DATABASE_URL, plus KEEP_ALIVE_TIMEOUT=65000 (see the common pitfalls below).

    Then give myapp-ec2-role permission to read the /myapp/* parameters and to pull from the myapp ECR repository. Use the AWS-managed AmazonEC2ContainerRegistryReadOnly policy, plus a small inline policy that allows ssm:GetParameter on that path. Because the parameter is encrypted, the role also needs permission to decrypt it with its KMS key (KMS is the AWS Key Management Service, which holds encryption keys; the default AWS-managed key needs no extra rule).

  5. Write the user-data script — the boot recipe

    User data is a script that runs once, as the root user, the first time a server boots. With it, a brand-new machine turns itself into one of your app servers while nobody is logged in:

    user-data.sh — the shape of it
    #!/bin/bash
    set -euxo pipefail
    # 1. install Docker and the AWS CLI
    # 2. read which version to run, and its settings (never hard-code them)
    TAG=$(aws ssm get-parameter --name /myapp/image-tag --query Parameter.Value --output text)
    aws ssm get-parameter --name /myapp/env --with-decryption \
      --query Parameter.Value --output text > /etc/myapp.env
    # 3. log in to ECR and start the container on port 80
    docker run -d --name myapp --restart unless-stopped \
      --env-file /etc/myapp.env -p 80:3000 $REGISTRY/myapp:$TAG

    The container is published on port 80, because the target group sends traffic to port 80 and web-sg allows port 80 from the ALB. If the boot fails, read the log at /var/log/cloud-init-output.log.

  6. Create the launch template

    Go to EC2 → Launch templates → Create. Fill it in like the server in Lesson 7 (IMDSv2 is the safer version of the metadata service explained below; gp3 is a type of SSD disk): Ubuntu 24.04, t3.small, web-sg, the myapp-ec2-role profile, a 20 GB encrypted gp3 disk, and IMDSv2 required. Paste the script above into User data. There are two differences from Lesson 7:

    • Metadata hop limit: 2. The instance metadata service (IMDS) is a local address where a server gets its role credentials. A container is one network hop further away from it than the host. With the default limit of 1, your app inside Docker cannot get the role's credentials, and every AWS call fails with “could not load credentials”.
    • No key pair, on purpose. Production servers should not accept SSH logins (SSH is the tool for logging in to a server from far away). In Lesson 18 you will get a shell through SSM Session Manager instead.
  7. Create the Auto Scaling group

    Go to EC2 → Auto Scaling groups → Create, and use the launch template. These settings matter most:

    • Two subnets in two AZs, min 2, desired 2, max 6. If one whole data centre is lost, one server is still running.
    • Attach it to myapp-tg, and turn on ELB health checks. Then the group replaces servers that the load balancer reports as unhealthy. Without this, the group only checks the EC2 machine. It would miss a server whose machine is fine but whose app is dead.
    • Health check grace period 180 s. Do not judge a new server for 3 minutes while it boots and pulls the image. Make this longer than your real boot time.
  8. Add the scaling policy

    On the same screen, choose Target tracking, the metric Average CPU utilisation, and the target 50. This means: “keep the average CPU across the group near 50%.” When the CPU is above 50%, the group adds servers. When it is well below, the group removes servers. AWS creates and manages the CloudWatch alarms for you (CloudWatch is the AWS monitoring service).

  9. Watch it come alive

    The group's Activity tab shows “Launching a new EC2 instance…”. If something fails, it also shows why. After 2–4 minutes, the target group's Targets tab shows both servers as healthy. Then open the ALB's DNS name in a browser to reach the app.

Health checks done properly

The health endpoint decides which servers stay and which are replaced. Keep it honest and cheap:

app/api/health/route.ts
export const dynamic = "force-dynamic";   // never cache a health check

export function GET() {
  return Response.json({ ok: true, version: process.env.APP_VERSION ?? "dev" });
}
Shallow check (above)Deep check (also queries the database)
DetectsThe app process is stuck or has crashedApp alive and database reachable
RiskIf the database is down, the server returns errors but still counts as “healthy”A short database problem marks every server unhealthy at once. The ASG then replaces them all. A small incident becomes a total outage
Use it forThe ALB target-group checkA separate /api/ready for dashboards and alerts, not for removing servers

Remember this rule: a health check that can fail because of another service (a dependency) can cause a chain of failures. A health check answers “is this server broken?”. An alarm answers “is the whole system broken?”.

Choosing a scaling policy

PolicyHow it decidesUse when
Target tracking — CPUKeep average CPU at a targetThe default for apps that are limited by CPU. It is the simplest, and it is our choice
Target tracking — ALBRequestCountPerTargetKeep requests per server at a target (e.g. 500/min)Better when the app mostly waits (for example on the database) and the CPU stays low
Step scaling“CPU > 70% add 1; > 90% add 3”You want to decide yourself how strongly to react
Scheduled“9 a.m. weekdays: min 4”You know when traffic comes (a daily peak, a sale, a marketing email)
PredictiveLearns your daily and weekly pattern from past data, and adds servers before the load comesSteady, repeating traffic. It needs at least 24 hours of history, and it uses up to 14 days

Why scaling is never instant

A new server needs a few minutes. It must launch, boot, pull the image, and pass two health checks. So scaling reacts a few minutes after the load rises. There are three ways to be ready for sudden spikes: a higher minimum, a scheduled action before an event that you know about, and a lower CPU target (40% leaves more room than 70%).

A good rule is to scale out fast and in slowly. Imagine you remove a server during a short quiet moment, and five minutes later you need it again. You waste time and money. This up-and-down switching is called flapping. Target tracking already avoids it by waiting longer before it scales in.

Speed up boot with a pre-baked AMI

Installing Docker on every boot costs about a minute. When things are stable, set up one server and run aws ec2 create-image. This saves the server as a new AMI (a “golden image”). Put that AMI in the launch template. New servers then start ready in seconds. Packer is a tool that builds such images automatically. Terraform (Lesson 15) is a tool that creates cloud resources from code, and it can connect it all.

Deploying without downtime

With many servers, a deploy is no longer “log in with SSH and restart”. The clean pattern is replace the servers, do not change them:

  1. CI (a robot that builds and tests your code on every change) pushes the new image to ECR, tagged with the commit SHA.
  2. Update /myapp/image-tag to that SHA.
  3. Start an instance refresh on the Auto Scaling group (min healthy 100%, max healthy 110%). An instance refresh is an ASG feature that replaces every server with a new one, a few at a time. AWS waits for each new server to pass its health check.

A rollback is the same move. Put the previous SHA back in the parameter and refresh again.

Min healthy 100% with max healthy 110% means AWS starts a new server before it retires an old one, so capacity never drops. With a very small group, a larger maximum such as 150% or 200% makes sure there is room for an extra server. The ALB only sends traffic to healthy targets, so users never reach a server that is still starting. The deregistration delay of 30 seconds lets requests that are still running on a retiring server finish. Lesson 14 starts the refresh from GitHub Actions.

Make your app exit cleanly on SIGTERM

When a server is retired, Docker first sends SIGTERM (a polite “please stop” signal). It waits a short time (10 seconds by default). Then it sends SIGKILL (a forced stop). The app should stop taking new connections and finish the current ones. The Next.js server handles this by itself. A custom Node server needs a process.on("SIGTERM", …) handler that calls server.close().

Prove it: load test and kill a server

Do not trust a design until you have tried to break it. Two experiments turn diagrams into real confidence.

Experiment 1 — does it scale?

On your laptop (a staging copy, or a quiet window — this is real traffic)
  • hey -z 3m -c 200 https://yourapp.com/

    hey is a small tool that sends many requests and reports the speed. This command uses 200 connections at the same time for 3 minutes (install hey with brew; k6 is a good alternative). Meanwhile, watch the instance count of the Auto Scaling group grow in the console

Look at three numbers in the hey summary, and at one graph:

  • Requests per second. This is your measured speed. Use it to improve the capacity estimate above.
  • Latency percentiles (p50, p95, p99). p95 means that 95% of requests were faster than this number. Averages hide problems. The slowest 1% of users are the ones who complain.
  • Status code counts. Any 5xx (server error) means something broke under load.
  • In CloudWatch, look at the ALB's TargetResponseTime and the database's DatabaseConnections. The database usually fails first.

Experiment 2 — does it heal?

While the load test runs, terminate one of the group's instances in the console, as if a data centre lost a machine. The ALB needs a few failed health checks, so it takes about a minute to mark the server unhealthy. A few requests may fail during that time. After that, the other server keeps serving the users. The group sees that it has fewer servers than wanted, and it starts a replacement by itself.

If the site shows errors for much longer than that, you have found a real weakness. It is usually a keep-alive timeout, a health check that is too slow, or a min-size of 1. It is cheap to fix it now.

The common pitfalls that cause real 502s

Keep-alive timeout: the intermittent 502

A keep-alive connection is a connection that stays open so that many requests can reuse it. The ALB reuses connections to your servers. It closes a connection after 60 seconds without traffic. Node closes idle connections after only 5 seconds by default. So sometimes the ALB sends a request on a connection that Node has just closed. The result is an instant 502. It happens for about one request in thousands, and you cannot reproduce it on your laptop. The rule is: the server's keep-alive timeout must be longer than the load balancer's idle timeout.

With the Next.js standalone server, you set this with the KEEP_ALIVE_TIMEOUT=65000 line that is already in /myapp/env (the value is in milliseconds). A custom Node server sets server.keepAliveTimeout = 65_000 and a slightly higher headersTimeout. Any value above the ALB's 60 seconds works.

Sticky sessions

The ALB can keep one user on the same server with a cookie. This is called “stickiness”. It is tempting when sessions live in server memory. But it spoils the even sharing of load, and the user loses the session when that server is replaced. Use it only as a short-term bridge while you move sessions to Redis (Lesson 16). It is not a real solution.

WebSockets

A WebSocket is a long-lived two-way connection (used for chat and live updates). ALBs support WebSockets with no extra setup. But such a connection ties a user to one server for as long as it lasts. Idle connections are closed after 60 seconds unless the app sends small “ping” messages. For such apps, scale on the number of connections, not on CPU.

Real client IP

Behind an ALB, every connection comes from the ALB. The user's address is in the X-Forwarded-For header. Set your framework to trust that header only from inside the VPC (see the real-IP note in Lesson 10). If you do not, every rate limit and log shows the ALB's IP.

What this costs

ItemApprox. per month (prices differ a little by region)
Application Load Balancer, base~$0.0225/hour ≈ $16.50
ALB usage (LCUs: connections, bytes, rules)~$0.008 per LCU-hour (an LCU is the unit AWS uses to measure ALB use); a small app ≈ $2–8
2 × t3.small, 24/7~$32
Public IPv4 per server + per ALB zone~$3.60 each (4 ≈ $14)
Auto Scaling, launch templates, target groupsFree
Extra servers during a spikeBilled per second, only while they exist
ACM certificateFree

A realistic total for this tier is about $60–75 a month. That is much more than one server. What you buy is: no single point of failure, deploys without downtime, and room to grow. If you do not need this yet, one server with Nginx is a good choice. Do not add an ALB only because the course has a lesson about it.

Stopping the meter while learning

When you are not studying, set the ASG to --min-size 0 --desired-capacity 0 (with aws autoscaling update-auto-scaling-group …). It then removes every server. The ALB keeps charging you, so delete it too when you finish. Or run this whole tier for one weekend only and build it again with Terraform in Lesson 15.

Troubleshooting table

SymptomLikely causeFix
Targets show “unhealthy” in the target groupThe health-check path does not return 200, the port is wrong, web-sg does not allow the ALB, or the app is still startingRead the reason in the target's Health status details. Run curl on the path from the instance itself. Check that web-sg allows port 80 from alb-sg. If every target is unhealthy, the ALB still sends traffic to all of them (this is called fail-open)
502 Bad Gateway from the ALBThe server closed the connection early (its keep-alive timeout is shorter than the ALB idle timeout), or it crashed during a requestSet the Node keepAliveTimeout above 60 s (see the common pitfalls). Check the app logs for that time
503 Service UnavailableThe target group has no registered targets, for example because the Auto Scaling group has 0 serversCheck that the ASG launched instances (see Activity history) and that it is attached to the target group
504 Gateway TimeoutThe app took longer than the ALB idle timeout (60 s by default)Find the slow endpoint. Move long work to a background job
Instances are launched and terminated in a loopThey fail the health check before the app is ready, so the ASG removes themRaise --health-check-grace-period above the boot time. Read /var/log/cloud-init-output.log
Scaling never triggersThe metric is not moving, the policy uses the wrong metric, max-size is already reached, or the group is still in warm-upCheck the alarm state in CloudWatch and the group’s Max
Users get logged out randomlySessions are stored in server memory, and the ALB sends the user to another serverMove sessions to a cookie/JWT or to Redis. Use sticky sessions only as a short-term fix
Some users see old code, some newA rolling deploy is still running, or one server missed the updateDeploy with an instance refresh so all servers end up on the same version. Tag images with the commit SHA

Interview corner

What must be true of an application before you put it behind a load balancer?

It must be stateless. Sessions, uploads and data must live outside the server (cookies or JWT or Redis, S3, RDS). Settings must come from a shared place. Scheduled jobs must not run on every server.

ALB vs NLB?

An ALB works at layer 7. It understands HTTP, so it can route by host, path or header, and it handles TLS, redirects and WebSockets. An NLB works at layer 4. It handles TCP and UDP with very high speed and low delay, and it gives fixed IPs. I use an ALB for web apps and APIs. I use an NLB for protocols that are not HTTP, or when I need fixed IPs.

Explain how Auto Scaling replaces an unhealthy instance.

With ELB health checks turned on, the group treats a server that the load balancer marks unhealthy as unhealthy too. It terminates that server and starts a replacement from the launch template, so the wanted number of servers is restored. It waits for the grace period before it judges new servers.

Why can a database-dependent health check be dangerous?

If the shared dependency has a short problem, every server fails the check at the same time, and the group replaces all of them. A small problem becomes a total outage. A health check should test the server itself. The health of dependencies belongs in alarms.

You get random 502s, about 1 in 2,000 requests. Nothing in the app logs. Why?

This is the classic keep-alive mismatch. The backend closes idle connections early (Node waits 5 s by default), before the ALB's 60 s idle timeout. So the ALB sometimes reuses a connection that is already closed. I set the backend keep-alive timeout above the ALB idle timeout.

How do you deploy a new version with zero downtime behind an ASG?

I build an image that never changes after it is built. I update the launch template, or the parameter it reads. Then I run an instance refresh. It starts new servers before it retires old ones, waits for health checks, and lets running requests finish (connection draining). A rollback is the same steps with the previous version.

1,000 concurrent users — how many servers?

First I turn users into requests per second (users divided by seconds between actions). Then I measure one server's speed with a load test and divide. I add extra room for spikes and for the loss of one AZ. Finally I check the number of database connections at the maximum number of servers, because that is often the real limit.


Practice task before Lesson 13

  1. Check your app with the statelessness table. Write down anything that would break with two servers, and fix it or make a note of it.
  2. Build the tier: certificate, target group, ALB, parameters, launch template, ASG, policy. Wait until both targets are healthy and load your site via the ALB DNS name.
  3. Break the health check on purpose (change the path in the target group to /nope). Watch the targets turn unhealthy. Notice that the ALB still sends traffic to them. When all targets are unhealthy, the ALB tries all of them (this is called fail-open). Restore the path quickly. With ELB health checks on, the ASG will soon start to replace the “unhealthy” servers.
  4. Run the load test and the terminate-a-server experiment. Record requests/sec, p95 latency, and how long the replacement took.
  5. Publish a new image tag, update /myapp/image-tag and run an instance refresh. Confirm the site stayed up the whole time.
  6. Work out the monthly cost of your setup at min-size 2, and at max-size 6 for one hour a day. Which line is the biggest?
Optional stretch

Add a second listener rule that forwards /api/* to a separate target group and a fixed 404 response for /admin from the internet. Rules are how one ALB becomes the front door for a whole platform.

Conclusion

You now run the same kind of setup that large sites use, only smaller: one address, many servers that are easy to replace, automatic repair, and automatic growth.

  • ALB = listener + rules + target group + health checks. It ends HTTPS with a free ACM certificate.
  • Auto Scaling = launch template + group + policy. Servers are cattle, made from a recipe with zero manual steps.
  • Stateless first. Sessions, files, data and cron jobs must live outside the web servers.
  • Health checks test the server, not its dependencies. Otherwise a short problem becomes an outage.
  • Do the maths (requests per second, capacity per server, and the number of database connections at maximum size), and test by breaking things.

The ALB gives you a long, ugly DNS name. Next, we connect your real domain to it, and to CloudFront, with Route 53.


End of Lesson 12. Next: Lesson 13 — Route 53: Production Domain.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series