A 3-tier SaaS on AWS for $57 a month

A load balancer, one small server and Postgres, with the server and the database in one Availability Zone. I put the server in a public subnet to skip the NAT Gateway, and wrote down what each shortcut saves and what it risks.

The whole setup, and its monthly bill

Take a small SaaS: 2,000 people use it in a month, and at the busiest hour about 100 of them are online at once. I wanted to see how cheap I can run that on AWS and still keep the usual three tiers: a load balancer in front, app servers behind it, Postgres at the back.

The setup in the animation above costs $57.12 a month in us-east-1. The server sits in a public subnet, the server and the database run in one Availability Zone, and there's no NAT Gateway, no second server, no standby database and no CDN. The NAT Gateway alone would cost more than the server behind it.

What the app needs

The app itself is nothing special. People sign up and log in, create and edit records in Postgres, and upload files up to 5 MB. It runs in one AWS region. A few minutes of downtime now and then is fine, there's no SLA to meet, and I don't want to spend my evenings looking after servers.

The setup

Three tiers, from the outside in:

  • Route 53 for DNS, and an Application Load Balancer that ends HTTPS with a free certificate from ACM [1].
  • EC2 instances in an Auto Scaling group, starting with one t4g.small.
  • RDS for PostgreSQL on a db.t4g.micro, and a private S3 bucket for the files.

It all lives in one VPC. The server and the database run in a single Availability Zone, but AWS still wants a second zone for two things. An Application Load Balancer needs subnets in at least two zones [2], and the database's subnet group does too, even when the database itself runs in one [3]. So the VPC has a second zone with a public and a private subnet. The load balancer has a node there, and the private subnet stays empty.

No NAT Gateway

The usual advice is to put app servers in a private subnet, where nothing on the internet can reach them. The server still has to reach out, though: OS updates, an email API, a payment provider. A private subnet has no way out on its own, so that traffic goes through NAT.

A NAT Gateway costs $0.045 an hour whether anything goes through it or not, plus $0.045 for every GB it handles [4]. That's $32.85 a month before any data, and the t4g.small it would serve costs $12.26 [5]. Done by the book, with a NAT Gateway in each zone, the pair comes to $74.35 a month with their public IPs and 30 GB of traffic.

Three ways out to the internet, and what each costs a month

These were my options:

  1. Private subnet with a NAT Gateway: $37.18 a month with its public IP and 15 GB of traffic.
  2. Private subnet with a NAT instance, a t4g.nano doing the same job: $7.36. It's one more server to patch, and when it dies, the app servers lose the internet until I fix it.
  3. Public subnet, locked down with security groups. The server gets a public IPv4 address, which AWS bills at $0.005 an hour, $3.65 a month [6].

I went with the third.

Security groups do the locking

With a public IP, anyone on the internet can try to reach the server, so the security groups decide who gets in:

  • The load balancer takes HTTPS on port 443 from anywhere.
  • The server takes app traffic only from the load balancer's security group.
  • SSH on port 22 works only from my IP address.
  • The database takes connections on port 5432 only from the server's security group, and it has no public address at all.
Who can come in, and from where

A security group drops anything that doesn't match a rule, so a request that goes straight to the server's IP never gets an answer. What worries me is a mistake. One wrong rule and the server is open to the whole internet, which can't happen to a server with no public address. For a small app I take that risk to save $33 a month.

One server, sometimes two

The app runs on EC2 from a launch template, in an Auto Scaling group with a minimum of 1, a maximum of 2 and a target tracking policy that keeps the average CPU at 60%.

One t4g.small is enough for this traffic, and I still put it in an Auto Scaling group for two reasons. When the server dies, the Auto Scaling group starts a new one on its own. When a spike pushes the CPU up, it adds a second server and removes it after the spike. With a maximum of two, the most a spike can add to the bill is one more t4g.small, $12.26 a month.

A traffic spike, a dead server and a dead zone

Two details decide how well that works. By default the Auto Scaling group only watches EC2 status checks, so if the app hangs on an instance that's still running, nothing happens. The load balancer runs its own health checks, set up in its target group. If you tell the Auto Scaling group to use them, it replaces that instance too [7]. And with a minimum of one, a replacement still means a few minutes of downtime: the new instance has to boot and then pass 5 health checks, 30 seconds apart, before the load balancer sends it traffic [8].

Since a second server can show up at any time, the app can't keep anything in memory or on its own disk. Sessions go in the database or in signed tokens like JWTs. I left out Redis and ElastiCache on purpose, because a cache is one more thing to pay for and look after.

The t4g instances also run on CPU credits. A t4g.small can use 20% of its two cores all the time, and above that it spends credits. It starts in unlimited mode, where AWS bills the extra at $0.04 per vCPU-hour [9] [5]. If the CPU sat at 50% around the clock, that would add $17.52 a month, more than the instance itself. An app that's quiet most of the day never gets close.

One database, one zone

The database is RDS for PostgreSQL on a db.t4g.micro, Single-AZ, in the private subnet, with no read replicas.

Multi-AZ would keep a standby copy in the second zone and switch to it, usually in 60 to 120 seconds, when something goes wrong in the first one [10]. It also doubles the price of the instance and its storage, from $13.98 to $27.96 a month. For this app I'd rather wait until AWS fixes the zone.

Automated backups stay on. RDS doesn't charge for backup storage up to the size of the database [11], so for a 20 GB database seven days of backups cost nothing, and I can restore the database to any point in those seven days. Turning them off would save no money, and it deletes the automated backups that already exist [12].

Files go straight to S3

Uploads go to a private S3 bucket. The server hands the browser a presigned URL, and the browser uploads the file straight to S3, so files never pass through the server. Downloads work the same way. A URL signed with the server's instance role stops working when those temporary credentials expire, which can be within 6 hours [13].

I left out CloudFront. At this traffic it would be free, because the always-free tier covers 1 TB and 10 million requests a month [14], but it's one more thing to set up, and S3 on its own is fast enough here.

The bill

On-demand prices in us-east-1, with 730 hours in a month:

LinePer month
Load balancer, 730 hours$16.43
Load balancer usage, about 0.2 LCU$1.17
EC2 t4g.small$12.26
RDS db.t4g.micro, Single-AZ$11.68
Public IPv4: 2 for the load balancer, 1 for the server$10.95
Storage: 20 GB for the server, 20 GB for the database, 10 GB in S3$4.13
Route 53 hosted zone$0.50
Backups, two alarms, data out under 100 GB$0.00
Total$57.12

The biggest line is the load balancer. With its two public IPv4 addresses it comes to $24.90, 44% of the bill [15]. The public addresses alone are $10.95, almost a fifth.

The two alarms are one for when the load balancer has no healthy server and one for when the database runs low on disk. The first 10 alarms are free [16], and the scaling policy makes two of its own.

What I left out

Everything the AWS docs would add, with what it would cost:

Left outAdds per monthWithout it
Private subnets, a NAT Gateway in each zone$70.70the server has a public IP
A second server in the other zone$13.86when the server dies, a few minutes of downtime
A standby database, Multi-AZ$13.98when the zone goes down, the database goes with it
CloudFront$0.00files come straight from S3
All of it$98.54

The NAT line is $74.35 for the two gateways minus the $3.65 the server no longer pays for its public IP. Turn them on in the animation and watch the bill:

The same app built by the book

By the book it comes to $155.66 a month, almost three times as much, and most of the difference is the two NAT Gateways.

What can go wrong

  • If the zone goes down, the whole app is down until AWS brings it back. When one zone in Tokyo lost its cooling, most servers there were running again after about six hours [17].
  • The Auto Scaling group can only replace a dead server if AWS can start new instances. During one big outage in us-east-1, new EC2 launches failed for about eight hours [18].
  • Two servers is the ceiling. That's plenty for this traffic and not enough for ten times more.
  • Monitoring is basic: two alarms and the default 5-minute EC2 metrics.
  • The server has a public IP, so one wrong security group rule exposes it.

When it grows

None of these need a rebuild. If the app grows, I'd add them in this order:

  1. Move the servers into private subnets, with a NAT Gateway in each zone.
  2. Turn on Multi-AZ for the database.
  3. Let the Auto Scaling group use both zones, with a minimum of two servers.
  4. Put CloudFront in front of static files and uploads.
  5. Add proper monitoring and alerts.

Sources

  1. Amazon Web Services, “AWS Certificate Manager pricing”.
  2. Amazon Web Services, “Application Load Balancers”, Elastic Load Balancing documentation. See Availability Zone subnets.
  3. Amazon Web Services, “Working with a DB instance in a VPC”, Amazon RDS User Guide.
  4. Amazon Web Services, “Amazon VPC pricing”. See NAT Gateway and Public IPv4 Address.
  5. Amazon Web Services, “Amazon EC2 On-Demand Pricing”. US East (N. Virginia), Linux.
  6. Barr, J., “New - AWS Public IPv4 Address Charge + Public IP Insights”, AWS News Blog, July 2023.
  7. Amazon Web Services, “Health checks for instances in an Auto Scaling group”, Amazon EC2 Auto Scaling User Guide.
  8. Amazon Web Services, “Health checks for Application Load Balancer target groups”, Elastic Load Balancing documentation.
  9. Amazon Web Services, “Unlimited mode for burstable performance instances”, Amazon EC2 User Guide.
  10. Amazon Web Services, “Failing over a Multi-AZ DB instance for Amazon RDS”, Amazon RDS User Guide.
  11. Amazon Web Services, “Amazon RDS for PostgreSQL pricing”. See Backup storage costs.
  12. Amazon Web Services, “Backup retention period”, Amazon RDS User Guide.
  13. Amazon Web Services, “Download and upload objects with presigned URLs”, Amazon S3 User Guide.
  14. Amazon Web Services, “Amazon CloudFront pricing”.
  15. Amazon Web Services, “Elastic Load Balancing pricing”. See Application Load Balancer.
  16. Amazon Web Services, “Amazon CloudWatch pricing”.
  17. Amazon Web Services, “Summary of the Amazon EC2 and Amazon EBS Service Event in the Tokyo (AP-NORTHEAST-1) Region”, August 2019.
  18. Amazon Web Services, “Summary of the Amazon DynamoDB Service Disruption in the Northern Virginia (US-EAST-1) Region”, October 2025.

Get the next article by email

I send one email when a new article is out, and nothing else.

Keep reading

Cache stampede

Soon

A popular key expires and ten thousand requests hit the database in the same second.

System Design

Design a URL shortener

Soon

The classic warm-up question, and the follow-ups that make it hard.

Interview questionsSystem Design