AWS ALB vs Nginx load balancing: when you need which

AWS Application Load Balancer vs Nginx for a Node.js app: features, cost, high availability and TLS compared, when each one wins, and when to run both.

"Should we use an ALB or Nginx?" comes up the moment a product outgrows its first server. Both can terminate HTTPS, route requests by path and spread traffic across several Node.js processes. The difference is what they are: Nginx is software you run on a server you manage; an Application Load Balancer is a managed AWS service that runs across several Availability Zones and that you never patch.

That difference decides most of the answer. Here's how they compare feature by feature, what an ALB really costs, when each one wins, when running both makes sense, and the settings that stop the infamous random 502s between an ALB and Node.js.

Part of my guide to AWS infrastructure for small SaaS teams.

TL;DR#

  • One server: use Nginx. It's free, does HTTPS with Let's Encrypt, caching, rate limiting and static files, and can balance across processes or a couple of servers.
  • Two or more instances, Auto Scaling or ECS: use an ALB. It's highly available across AZs by design, gets free auto-renewing TLS certificates from ACM, health-checks targets and plugs into WAF.
  • An ALB costs about $25–30 a month at low traffic in us-east-1, including its public IPv4 addresses.
  • Run both only when you need something the ALB can't do on the instances, like response caching. Otherwise ALB → Node.js directly is simpler.
  • Set Node's keepAliveTimeout above the load balancer's idle timeout (65 seconds for the ALB's default 60), or you'll see intermittent 502s.

What each one is#

Nginx is a web server and reverse proxy. You install it on a Linux server, and it does whatever its configuration says: terminate TLS, serve files, cache responses, rate-limit clients, and balance requests across upstream servers. It's fast, free and endlessly flexible. It's also one more server process you configure, patch and keep alive, and on its own it's a single point of failure.

An Application Load Balancer is AWS's managed layer 7 (HTTP) load balancer. You don't see servers: you see listeners, rules and target groups. AWS runs it in every Availability Zone you enable, scales it with your traffic, and replaces failed nodes. You give up most of Nginx's flexibility in exchange for never thinking about the load balancer's own uptime.

Side by side#

Nginx (open source) AWS ALB
Runs on Your server AWS, across 2+ Availability Zones
High availability of the balancer itself Up to you (a second server plus failover) Built in
TLS certificates Let's Encrypt with Certbot ACM: free public certificates, renewed automatically
Routing Anything: paths, hosts, headers, regex, rewrites Path, host, header, method, query string and source IP rules
Health checks Passive only (max_fails); active checks need NGINX Plus Active, per target group
Rate limiting limit_req, per second, per key Only through AWS WAF rate-based rules, counted over minutes
Response caching proxy_cache None (use CloudFront)
Static files Serves them directly No
WebSockets, HTTP/2, gRPC Yes Yes
Sticky sessions ip_hash or hash Load balancer or application cookies
Weighted routing (canary, blue-green) weight per server Weighted target groups
Auto Scaling and ECS integration Manual Native: targets register themselves
Web application firewall ModSecurity or similar, self-managed AWS WAF, managed rule groups
Built-in login (OIDC, Cognito) Third-party modules Yes
Price Free, plus the server it runs on About $16 a month plus usage

Both carry long-lived connections such as WebSockets and Server-Sent Events, as long as the idle timeout outlasts your heartbeat. Server-Sent Events vs WebSockets covers the heartbeat and the proxy settings a stream needs.

What an ALB costs#

In us-east-1, an ALB is billed for three things:

  • Hours: $0.0225 an hour, about $16.43 a month.
  • LCUs (Load Balancer Capacity Units): $0.008 per LCU-hour. One LCU covers, per hour, the highest of 25 new connections a second, 3,000 active connections a minute, 1 GB of traffic to EC2 targets or 1,000 rule evaluations a second. A small API uses about one: about $6 a month.
  • Public IPv4 addresses: an internet-facing ALB has at least one per enabled AZ, at $0.005 an hour each: about $7.30 a month for two AZs.

So a small, internet-facing ALB costs roughly $25–30 a month, rising with traffic. Nginx costs nothing on a server you already run, but making Nginx itself highly available means a second server and failover, and on AWS there's no shared virtual IP to float between them, so you'd script an Elastic IP move or put a load balancer in front anyway.

When Nginx alone is enough#

If everything runs on one server, an ALB adds cost and a hop without removing the single point of failure: the server is still one machine. Nginx on that server is the right tool. It handles HTTPS, compression, rate limits and static files, and it can balance across several Node.js processes or containers on the box, or across a couple of servers:

upstream api {
    least_conn;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3002 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on; # Nginx 1.25.1+; on older versions: listen 443 ssl http2;
    server_name api.example.com;

    # ssl_certificate and ssl_certificate_key, from Certbot

    location / {
        proxy_pass http://api;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_next_upstream error timeout http_502 http_503;
    }
}
  • least_conn sends each request to the upstream with the fewest active connections, which suits APIs with uneven request times better than the default round robin.
  • max_fails and fail_timeout are Nginx's passive health check: after 3 failures within 30 seconds, an upstream is skipped for 30 seconds.
  • keepalive with HTTP/1.1 and an empty Connection header reuses connections to Node.js instead of opening one per request.
  • proxy_next_upstream retries a failed request on the next upstream. Nginx never retries non-idempotent requests like POST unless you explicitly allow it.

The full setup, with Let's Encrypt, is in Nginx reverse proxy for Node.js with free SSL.

When you need an ALB#

Move to an ALB when any of these is true:

  • You run two or more instances and the product can't go down when one does. Put them in at least two AZs and the ALB stops sending traffic to an unhealthy one within seconds.
  • Instances come and go: an Auto Scaling group, ECS tasks or EKS pods register with the target group by themselves.
  • You want TLS you never touch. ACM certificates are free, renew automatically and never sit on your servers.
  • You need AWS WAF in front of the app, for managed rules against common attacks, bot control or rate-based blocking.
  • Nobody wants to own the proxy. No config to patch, no Nginx process to monitor.

When to run both#

ALB → Nginx → Node.js is common, and often unnecessary. Each hop adds latency, configuration and a place for timeouts to disagree. It earns its place when the instances need something only Nginx does:

  • Response caching on the instance (or put CloudFront in front of the ALB instead).
  • Per-second rate limiting by client or API key.
  • Serving large static files from the instance's disk.
  • Complex rewrites or legacy routing the ALB's rules can't express.

If you do run both, Express sees two proxies: set app.set('trust proxy', 2), so req.ip is the visitor's address and not the load balancer's.

Setting up an ALB for Node.js properly#

A few settings make the difference between a smooth ALB and a flaky one.

Match keep-alive timeouts. This one causes the classic "random 502" with Node.js. The ALB keeps idle connections to your targets open for its idle timeout, 60 seconds by default. Node's HTTP server closes idle connections after 5 seconds. Every so often the ALB sends a request down a connection Node is closing at that very moment, and the user gets a 502. Make Node wait longer than the load balancer:

const server = app.listen(3000);

// Longer than the ALB's 60-second idle timeout (and Nginx's 60-second upstream keepalive), so the
// load balancer always closes idle connections first.
server.keepAliveTimeout = 65_000;
server.headersTimeout = 66_000; // must be longer than keepAliveTimeout

Health checks that mean something. Point the target group at a /health route that checks the database. The defaults (every 30 seconds, 5 successes to become healthy) make a new instance wait 2.5 minutes before it takes traffic. An interval of 10 seconds and a healthy threshold of 2 to 3 is usually better.

Shorter deregistration delay. When a target is removed, the ALB waits for in-flight requests to finish, 300 seconds by default. For an API whose requests take seconds, 30 makes every deploy minutes faster.

Redirect HTTP to HTTPS with a listener on port 80 whose default action is a redirect, rather than letting the app do it.

Trust the proxy. The ALB adds X-Forwarded-For and X-Forwarded-Proto; with app.set('trust proxy', 1) Express reads the visitor's IP and the original protocol from them.

Only the ALB reaches the app. The instances' security group should admit the app's port from the ALB's security group, and nothing else. VPCs, subnets and security groups explained shows that chain.

Moving from Nginx to an ALB without downtime#

  1. Lower your DNS record's TTL to 60 seconds a day ahead.
  2. Create the ALB, its target group and an ACM certificate (DNS validation is quickest).
  3. Register the existing instances and wait for them to show healthy.
  4. Point DNS at the ALB, as a Route 53 alias record if you use Route 53.
  5. Keep Nginx running until traffic to it stops, then remove it, or keep it behind the ALB if you need what it does.

How to choose#

Your situation Use
One server, one or a few apps Nginx
Two or more instances, or an Auto Scaling group ALB
ECS, or EKS with the AWS Load Balancer Controller ALB
Response caching or per-second rate limits on the instances Nginx, behind an ALB if you need high availability
Static IP addresses, TCP or UDP A Network Load Balancer instead
Tight budget, and a minute of downtime during a reboot is acceptable Nginx

FAQ#

Is an AWS ALB better than Nginx?#

Neither is better in general. An ALB is a managed, highly available load balancer that integrates with Auto Scaling, ECS, ACM certificates and AWS WAF, and costs about $25–30 a month at low traffic. Nginx is free and more flexible, with caching, rate limiting and static files, but runs on a server you manage. Use Nginx on a single server and an ALB once you run several instances.

Do I need Nginx behind an AWS load balancer?#

Usually not. An ALB can send traffic straight to Node.js, and it handles TLS, routing and health checks. Keep Nginx behind it only when the instances need something the ALB can't do, such as response caching, per-second rate limiting or serving static files, and then set Express's trust proxy to 2.

How much does an AWS Application Load Balancer cost?#

In us-east-1, $0.0225 an hour (about $16 a month), plus $0.008 per LCU-hour for usage, plus $0.005 an hour for each public IPv4 address, at least one per Availability Zone. A small internet-facing ALB in two zones comes to roughly $25–30 a month, rising with traffic.

Why does my AWS load balancer return random 502 errors with Node.js?#

Usually because Node.js closes idle keep-alive connections after 5 seconds, while the ALB keeps them for 60, so the ALB occasionally reuses a connection Node is closing. Set server.keepAliveTimeout to 65 seconds and server.headersTimeout slightly higher, so the load balancer always closes idle connections first.

Can Nginx do load balancing?#

Yes. An upstream block lists the servers, and Nginx balances requests across them with round robin, least connections or IP hash, retrying failed requests on another server. Open-source Nginx has passive health checks only; active health checks require NGINX Plus.

Muhammad Huzaifa Abdulahad
Written by

Muhammad Huzaifa Abdulahad

Full-Stack Software & DevOps Engineer

I build SaaS platforms with Node.js, NestJS, TypeScript and PostgreSQL, and run them on Linux, Docker, AWS and CI/CD — from the first line of code to production.

Related articles

See all posts →

Have a complex idea? Let's build it together.

From MVPs to scalable products, let's turn your idea into reliable software built for real-world impact.