Introduction ─ We had AI read our bill, and the waste kept turning up
I suspect most people only look at the total on their monthly AWS bill. That was true at MOOBON for a long time, too. The routine was to glance at "$X total, yeah, about what I expected" and close it.
One day, on a whim, I fed our AWS bill to an AI and had a detailed discussion with it about our cost structure. What surfaced, one item after another, were "costs we were reliably paying every month without realizing we were using anything". The big win was that it listed everything several times faster than eyeballing Cost Explorer's usage-type view line by line, and without missing anything.
The waste we found ultimately came to $310 a month. This article digs into each item one at a time from three angles: how much we were paying / why it was wasteful / how we got it back. The next section lays out a map by phase, and then we get into the details of each tactic.
* The $310 is the total of tactics #1–#5, #7, and #8. Tactic #6 (migrating EC2 from AL2 to AL2023 + shrinking EBS, an additional -$125/month for moobon alone) is covered on its own in a separate article, so it isn't included in this article's total.
At the time of writing (2026-05), because of the Amazon Linux 2 → 2023 rehosting migration, old EC2 instances are running in parallel, along with additional EIPs, which temporarily inflates our current costs by a few tens of dollars a month. We expect this to drop off naturally once the migration is complete, but please treat the numbers in this article as conservative figures that include the migration overhead.
1. Two phases of review ─ From "feeling like we were on it" to "found it, got it back"
Certified, 10+ years on AWS, and still just "feeling like we were on it"
I myself hold the AWS Solutions Architect – Associate (SAA) certification and have been using AWS for more than 10 years. I was convinced I had most of the cost optimization basics covered.
And in fact, we had been managing EC2 / RDS / ECS instance types and buying Reserved Instances (RIs) in a planned way. That left me with the feeling that "we're doing what needs to be done" ── that was Phase 0.
But my routine with the bill was to skim the total and close it. I had never traced the breakdown by service or by usage type line by line. I had convinced myself that "we're doing most of what we should be doing", and realizing that was the starting point for this article. As I'll explain later, what actually shattered that assumption was the moment I had AI analyze our billing data. Being certified and having used AWS for more than 10 years doesn't mean you'll see the waste just by combing through the bill by hand.
Rethinking our VPC setup uncovered a lump of fixed costs
When I fed our AWS billing data to an AI and had it point out problems, the first thing that surfaced, one item after another, was "waste we'd been paying without noticing" around the VPC. You could probably reach the same conclusions by eyeballing Cost Explorer's usage-type view, but the AI also pointed out "how much it adds up to" and "what's wasteful given the billing structure" in the same breath, so decisions came dramatically faster. That's how this whole project started.
- We had 3 ALBs running (one dedicated to a client's service, one for EC2, and one for ECS). With listener rules, a single ALB would have been enough. Between the ELB monthly charge and Public IPv4, that was -$57/month of waste.→ Go to tactic #1
- We were paying $107 every month for a NAT Gateway (hourly $45 + data processing $62). Outbound traffic from ECR pulls, OS patches, ECS task launches, and so on had quietly piled up, driving up the data processing volume. Replacing it with an EC2 NAT (FCK-NAT) brought it down to about $8/month. A big -$99/month cut.→ Go to tactic #2
- As a side effect, we noticed multiple WAFs were running. We consolidated the Web ACLs into one along with the ALB consolidation. -$16/month. A discovery that wasn't in the original plan.→ Go to tactic #3
Looking across the EC2 / ECS / RDS / backup layers, we weren't optimizing them as a whole
Once Phase 1 had cleaned up the VPC, we turned our attention to the compute and storage layers. The RI purchases and spec management from Phase 0 were certainly paying off, but it became clear that they were optimized in isolation and not combined to full effect.
- Our RI coverage was full of holes. After migrating from EC2 to ECS Fargate at some point, the EC2 RIs we had bought were sitting idle. We moved part of our stable production workload back from Fargate to EC2 and set it up to use the RIs via an ECS Capacity Provider.→ Go to tactic #8
- ECS tasks were running with public IPs assigned (billed since 2024-02). We moved them to private subnets and off the Public IPv4 bill.→ Go to tactic #4
- Fargate was more expensive than we thought ($263/month for moobon alone). We're considering putting staging on Fargate Spot at 70% off, shifting production toward EC2 RIs in combination with tactic #8, and covering whatever on-demand usage remains with Compute Savings Plans.→ Tactic #7 + next steps
- EBS had quietly bloated. During the AL2 → AL2023 migration, we switched to gp3 and put the volumes on a diet (100GB → 30GB). That delivered the biggest single impact: -$125/month / -75% on EC2-Other.→ Go to tactic #6
- Logs were scattered all over the place. Nginx logs and DB dumps had been piling up on EBS, so we standardized on offloading them to S3 right away via logrotate / cron. This worked hand in hand with shrinking EBS.→ Details in the AL2 → AL2023 migration article (section 6)
In parallel with the above, we also trimmed the unnecessary metrics the CloudWatch Agent was publishing on its default settings (tactic #5).
From the next section on, we dig into each of these tactics one at a time from three angles: "how much we were paying / why it was wasteful / how we got it back". If you'd rather read by phase, use the links above to jump to the corresponding tactic.
2. Consolidating 3 ALBs into 1 (23 listener rules)
What we did
We consolidated the per-service ALBs from 3 down to 1. Without changing the number of domains, we created 23 listener rules on the HTTPS listener (443) that route traffic to the right target group based on the host header (Host: example.com) + path (/api/*, etc.). The HTTP listener (80) is kept simple, with a single rule that just redirects to HTTPS.
Why it works
Each ALB costs about $16/month in fixed fees + LCU charges. Run 3 of them and that's $48/month in fixed fees alone. Even with traffic consolidated onto one ALB, LCU charges stay at roughly the same level, so the fixed cost simply drops to a third.
Another easily overlooked factor is the Public IPv4 charge introduced in February 2024. An ALB deployed across multiple AZs (3 AZs in our case) consumes one Public IPv4 address per AZ, so we were paying for 3 ALBs × 3 AZs = 9 Public IPv4 addresses (= $32.85/month). Consolidating into one brings that down to 3 addresses = $10.95/month, which means a hidden -$22/month saving on Public IPv4 charges buried in the VPC bucket happens at the same time.
In other words, the total impact of tactic #1 is "-$35/month on ELB" + "-$22/month on Public IPv4" ≈ about -$57/month. It's easy to miss if you only look at Cost Explorer's by-service view, so the existence of the Public IPv4 charge carries a lot of weight when deciding whether to consolidate ALBs.
Gotchas
- Listener rule priority design: with 23 rules lined up, unless you strictly follow the "more specific rules go higher" principle, traffic ends up at unintended backends. Grouping redirect rules at the end is the safe bet.
- Duplicate health check paths across target groups: if backends are designed to respond on the same path, health checks will still pass when traffic is mistakenly mapped to a different service, making it hard to notice. Use service-specific paths like
/healthz. - Carrying over WAF / access logs: if you forget to port the WAF and logging settings attached to the ALBs being merged over to the new ALB, you'll lose compliance coverage after consolidation.
- DNS cutover timing: shorten the Route 53 TTL in advance (to 60 seconds or so) and make sure you can roll back before switching the Alias record.
Before / After
| Item | Before | After |
|---|---|---|
| Number of ALBs | 3 | 1 |
| Listener rules | A few per ALB | 23 rules consolidated on 443 / 80 only redirects to HTTPS |
| Public IPv4 addresses used by ALBs | 9 (3 ALBs × 3 AZs) | 3 (1 ALB × 3 AZs) |
| ELB monthly cost (moobon alone) | $77 | $42 (-$35 / -45%) |
| Public IPv4 monthly cost (from ALBs) | 9 × $3.65 ≈ $33 | 3 × $3.65 ≈ $11 (-$22 / -67%) |
| Total savings from tactic #1 | - | About -$57/month |
3. Replacing NAT Gateway with an EC2 NAT (FCK-NAT)
What we did
We replaced the managed NAT Gateway with an EC2-based NAT instance (the FCK-NAT pattern). It's a single t3a.micro instance based on Amazon Linux 2023, wrapped in an Auto Scaling Group (min=max=desired=1) so that it recovers automatically.
The actual Launch Template / UserData / IAM policy / ASG settings we use in production, the real story of rewriting the Launch Template six times, and the operational caveats are all covered in a separate article, "Pinning the Outbound IP of ECS ── Our Record of Adopting fck-nat in Production". This article focuses on verifying the cost savings.
I said "Auto Scaling", but in practice it's self-healing, not scaling. The moment the EC2 health check goes unhealthy, the ASG launches a replacement instance from the same launch template. All you have to do is point 0.0.0.0/0 → the NAT instance's ENI (or the instance) in the private subnet's route table. The FCK-NAT AMI handles everything built in, from IP forwarding and the NAT iptables rules to disabling the ENI's SourceDestCheck, so there's no need to touch OS settings on the server by hand. For Public IPv4, we associate an EIP to get a fixed IP.
Why it works (verified against moobon's actual bill)
A managed NAT Gateway in ap-northeast-1 costs $0.045/h × 720h = about $32/month in hourly charges, plus $0.045/GB of data processed. That's all there is to it "on paper". But looking at moobon's actual bill, outbound traffic (ECR pulls / OS patches / ECS task launches / external API calls, etc.) was sending the equivalent of 1.4 TB every month through the NAT, and we were paying $62 a month in data processing alone.
| Charge | Monthly (actual, 2025-11) |
|---|---|
APN1-NatGateway-Hours (hourly) | $44.64 |
APN1-NatGateway-Bytes (data processing) | $62.27 |
| Total (1 NAT Gateway / month) | $106.91 |
We thought it was around "$32 fixed + a few tens of dollars in data processing", but in reality we were paying over $100 a month for a single NAT GW. While we vaguely used it because "managed means convenient", the data processing charges had been creeping up ── that's what was really going on.
A t3a.micro + 8GB EBS NAT EC2, on the other hand, comes to about $8/month in total for the instance plus storage. EC2 data transfer is free within an AZ / within a region, and only traffic leaving the AZ is charged, so the billing structure stays light even when traffic is heavy.
Gotchas and availability
- "Down" doesn't mean "every request fails": even if the NAT EC2 goes down, the only thing affected is traffic from servers to the outside world (API calls / OS package updates / ECR pulls / integrations with external SaaS). Requests that complete entirely within ALB → app → DB (in-VPC resources like RDS / ElastiCache) are unaffected. In other words, whether a NAT outage immediately breaks user-facing responses depends on how your system is built. If most of your web app is designed to complete within in-VPC resources, a NAT failure stays limited to "only the processes that depend on external services partially fail".
- Recovery time: ASG self-healing takes 2–4 minutes. That's worse than the managed NAT Gateway's 99.99% SLA, but as explained above, it's "not a total outage" ── "only features that need outbound traffic stop working temporarily". Depending on requirements, that's acceptable even in production in many cases.
- The only configuration is the route table: the FCK-NAT AMI takes care of IP forwarding / iptables / disabling SourceDestCheck, so you never have to touch OS settings on the server. All you do is write one line in the route table:
0.0.0.0/0 → the NAT ENI. It's much less error-prone than building a NAT server by hand from Amazon Linux. - ENI switchover when the ASG replaces the instance: when the instance is replaced, the ENI changes. FCK-NAT ships with a startup script that automatically re-registers its own ENI in the route table. Check in advance that the route table ID and tags are passed correctly via the launch template.
- Bandwidth and connection limits:
t3a.microis burstable with a maximum of 5 Gbps, and its conntrack limit is modest too. For workloads that expect constant high traffic or large numbers of concurrent connections, considert3.smallor larger, or a network-optimized instance type. - Monitoring: set CloudWatch alarms on the NAT instance's NetworkOut / CPUUtilization, as well as an application-side metric for "external API call success rate". Since it isn't managed, this prevents the "we suddenly noticed only the external integrations were broken" scenario.
Before / After
| Item | Before | After |
|---|---|---|
| NAT setup | Managed NAT Gateway × 1 | EC2 t3a.micro × 1 (FCK-NAT, AL2023 + ASG self-healing) |
| NAT Gateway monthly cost (moobon actual, 2025-11) | Hours $44.64 + Bytes $62.27 = $106.91 | ─ (removed) |
| EC2 NAT monthly cost (t3a.micro + 8GB EBS) | ─ | About $8/month |
| Savings | - | About -$99/month (-93%) |
| Availability / impact during failure | Managed by AWS (SLA 99.99%) | ASG auto-recovery (2–4 min). Not a total outage, though: impact is limited to processes that need outbound traffic (API calls / ECR pulls, etc.) |
4. Consolidating multiple WAFs into 1
What we did (as a by-product of tactic #1)
This tactic was a by-product that wasn't in the original cost reduction plan. Once tactic #1 consolidated the ALBs from 3 to 1, it was only natural to consolidate the WAF Web ACLs attached to each ALB into one as well. In the process of merging the Web ACLs, we discovered duplicate rules and mismatched modes (a mix of Count / Block), so we cleaned up the rule configuration at the same time. The consolidated setup consists of these 8 rules:
| Priority | Rule | Type |
|---|---|---|
| P0 | AWSManagedRulesLinuxRuleSet | Managed |
| P1 | AWSManagedRulesSQLiRuleSet | Managed |
| P2 | AWSManagedRulesCommonRuleSet | Managed |
| P3 | AWSManagedRulesWordPressRuleSet | Managed |
| P4 | AWSManagedRulesPHPRuleSet | Managed |
| P5 | AWSManagedRulesBotControlRuleSet | Managed |
| P8 | allow-overseas-for-allowed-hosts | Custom Regex |
| P9 | block-from-overseas-countries | Custom And |
Why it works
A WAF Web ACL is billed at a $5/month base fee per ACL, plus $1/month per rule and $0.60 per million requests. Run multiple WAFs and the base fee stacks up with each one. If the same managed rule group is loaded into multiple ACLs, you're also paying rule charges for each of them ── an inefficiency that consolidation eliminates.
Gotchas
- Sequence it with the ALB consolidation: doing it alongside the ALB consolidation in #1 makes the work easier, since there are fewer resources to attach to.
- Custom rule priority: evaluate the managed rule groups (P0–P5) first and put our own geo-filtering custom rules after them. In reverse order, you risk blocking legitimate requests by mistake.
- Check the cost of BotControl: AWSManagedRulesBotControlRuleSet in standard mode comes with an additional $10/month + request charges (priced differently from the other managed rules). Decide whether to turn it on or off based on your requirements.
Before / After
| Item | Before | After |
|---|---|---|
| Number of Web ACLs | Multiple (one per ALB) | 1 (attached to the consolidated ALB) |
| Rules (consolidated ACL) | With duplicates | 8 (6 managed + 2 custom) |
| WAF monthly cost (moobon alone) | $31 | $15 |
| Savings | - | -$16/month (-52%) |
5. Moving ECS to private subnets (cutting Public IPv4)
What we did
We switched our ECS tasks from public subnets with directly assigned public IPs to private subnets with outbound traffic via NAT (the EC2 NAT). This eliminated the need for the Public IPv4 address that had been assigned to each task. Combined with the addresses cut by the ALB consolidation in #1, the number of billable Public IPv4 addresses dropped significantly.
Why it works
In February 2024, AWS started charging $0.005/h (≈ $3.65/month) for every Public IPv4 address. IPs that used to be handed out casually via "Auto-assign Public IP on the ENI" all became billable. In the month of writing (2026-04), MOOBON's APN1-PublicIPv4:InUseAddress came to $97.79 (the equivalent of 27 addresses per month), and APN1-PublicIPv4:IdleAddress to $5.51. Even with just 10 ECS tasks, a public placement costs $36 every month "just for holding IPs".
Gotchas
- ECR pulls go through the NAT: with private subnets, ECS tasks go through the NAT when pulling images from ECR. After switching to the EC2 NAT (tactic #2), the transfer cost itself is light, but Fargate task startup times tend to get slightly longer due to the NAT hop. If that matters at your scale, you could add Interface VPC endpoints for ecr.api / ecr.dkr / s3 / logs / secretsmanager to bypass the NAT, but we haven't evaluated that yet (it's listed under what we plan to tackle next).
- IP allowlists for external APIs (where this actually becomes the right architecture): payment APIs in particular (credit card, convenience store payment, bank transfer integrations, etc.) often require you to register and pin your source IP in advance as a business requirement. The thing to realize here is that the public IP assigned to an ECS Fargate task is dynamic and changes every time the task restarts, so it can't be used for allowlisting in the first place. With private subnets + outbound via the NAT's EIP, outbound traffic is funneled through the NAT's fixed EIP, which you can then register with the external party as your fixed IP. Alongside the cost savings, you automatically end up with the "right architecture" for external integrations that require allowlisting ── that's the other benefit of this tactic (for inbound APIs via the ALB, the ALB-side IPs apply as before).
Before / After
| Item | Before | After |
|---|---|---|
| Public IPv4 addresses used by ECS service tasks | 8 | 0 (all tasks go through the NAT) |
| Savings | 8 × $3.65/month ≈ about -$29/month | |
USAGE_TYPE on APN1-PublicIPv4:InUseAddress / :IdleAddress. It's a textbook example of a cost that "balloons without you noticing", so it's worth checking at least every six months.6. Trimming unnecessary CloudWatch Agent metrics
What we did
When we installed the CloudWatch Agent on our EC2 instances, we just ran it without really fine-tuning the configuration, so around 10 custom metrics per instance were being published. The only ones we actually used for alarms and incident analysis in practice were mem_used_percent and disk_used_percent, so we narrowed it down to just those two.
After the cleanup, moobon's config.json looks like this ── a minimal configuration with only mem and disk (retrieved from the actual instance via SSM to confirm):
{
"agent": {"metrics_collection_interval": 60},
"metrics": {
"append_dimensions": {"InstanceId": "${aws:InstanceId}"},
"metrics_collected": {
"mem": {"measurement": ["mem_used_percent"]},
"disk": {"measurement": ["disk_used_percent"], "resources": ["/"]}
}
}
}Why it works: from 10 per instance down to 2
When we started the cleanup, thanks to a haphazard initial setup, around 10 custom metrics per instance were being published. We cut this down to 2: mem + disk, used the standard EC2 metrics (free) for CPU, and stopped publishing disk IO, netstat, swap, processes, and the like, since we rarely used them.
| State | / instance | Total for 8 instances | Monthly (USD) |
|---|---|---|---|
| Before (haphazard setup) | 10 | 80 metrics | About $24 |
| After (mem + disk only) | 2 | 16 metrics | About $5 |
| Savings | −8 per instance, −64 metrics in total | About −$19/month | |
In absolute terms it's a small -$19/month, but CloudWatch custom metrics cost $0.30 per metric per month (for the first 10K) and grow in direct proportion to the number of instances. The habit of "keeping collection to the bare minimum" pays off proportionally as your fleet grows, so it's worth enforcing from day one.
Gotchas: none
For this tactic, there really aren't any gotchas. There's no point collecting custom metrics you don't use for alarms, and just continuing to collect the data consumes resources (a resident CWAgent + network + metric charges). It's perfectly fine to delete unused metrics first thing. Even if you keep them around because "we might need them someday", you can start publishing them right away by configuring them when the time comes.
7. Migrating EC2 from AL2 to AL2023 + gp3 + smaller EBS
I've written a separate, detailed article on this one. The highlights: we shrank EBS from 100GB gp2 to 30GB gp3, offloaded unnecessary backups and logs to S3 to clear out everything that had piled up on EBS, and costed out keeping 10 generations of AMI snapshots. For moobon alone, EC2 - Other went from $166 to $41 (-$125 / -75%), making it one of the biggest cuts in this whole project.
Migrating 14 sites / 28 WordPress installs from Amazon Linux 2 to 2023Before / After (contribution to this project)
| Item | Before | After |
|---|---|---|
| EBS volume | 100GB gp2 | 30GB gp3 |
| EBS monthly cost (per volume) | $12.00 | $2.88 |
| 10 generations of AMI snapshots | $4.68 | $1.44 |
| EC2 - Other monthly cost (moobon alone) | $166 | $41 (-$125 / -75%) |
8. Moving staging ECS to Fargate Spot
What we did
We switched the ECS tasks in our staging environment, used for pre-release testing, to Fargate Spot. We either set FARGATE_SPOT to 100% in the capacity provider strategy, or used base 0 + weight 1, so that every task launches on Spot.
Why it works
Fargate Spot is up to 70% cheaper than regular Fargate. For a 0.25 vCPU / 0.5 GB task in ap-northeast-1, regular Fargate is $0.0123/hour → Spot is around $0.0037/hour (varies over time). Per month, that's roughly $9 → $2.7 per task. If you keep about 5 staging tasks running at all times, that works out to nearly $30 off every month.
Gotchas (no issues in our case)
- 2-minute interruption notice: Spot tasks can be interrupted at AWS's discretion, but ECS restarts them automatically. For staging, it's a total non-issue (for production, it depends on the use case).
- Securing capacity: if you request large amounts of Spot capacity for an extremely high-demand instance type, you may not get it. It's well suited to designs that can scale up from a minimum task count of 0.
- Cases where we wouldn't recommend it for production: long-running jobs (video encoding, etc.) and APIs that require an SLA of 99.9% or higher. Long-running jobs have to start over when interrupted, and SLA-bound services lose SLA for the duration of the interruption.
I'm wondering whether, even for production tasks, running 2 Fargate Spot tasks side by side might be both cheaper and more resilient than 1 regular Fargate task. The price comes out lower at
$9 → $2.7 × 2 = $5.4, and if one gets interrupted the other keeps going, so availability goes up too (register both tasks in the ALB target group, and traffic can be routed to the other task during an interruption). I have yet to verify interruption frequency and AZ distribution, but for production workloads whose requirements allow it, it seems worth trying.9. Moving Fargate to EC2 to use our Reserved Instances
What we did
Some production workloads that originally ran on EC2 had been migrated to ECS Fargate, and as a result we ended up in an unexpected situation where EC2 Reserved Instances (RIs) we had bought in the past were sitting unused. RIs are a fixed cost you've already committed to, so you keep paying for them whether you use them or not.
So we moved the tasks we had been running on Fargate (three small 0.25 vCPU tasks + one 1 vCPU task, four in total) onto the spare t3a.small RIs as ECS on EC2 (switching the production ECS capacity provider to the EC2 launch type). I only meant to put the RIs to full use, but it ended up delivering far bigger savings than I expected.
Why it works: Fargate bills vCPU and RAM separately
What I realized when I actually did it was that Fargate pricing is higher than I had assumed. Fargate charges for vCPU and RAM separately by the hour, and the on-demand rates in ap-northeast-1 are as follows:
| Dimension | Rate (on-demand) |
|---|---|
| Fargate vCPU | $0.05056 / vCPU-hour |
| Fargate memory | $0.00553 / GB-hour |
In our case, we took 4 Fargate tasks in total (3 small tasks at 0.25 vCPU + 0.5 GB and 1 task at 1 vCPU + 2 GB) and split them across 2 t3a.small RIs as ECS on EC2. Here's the estimate:
| Configuration | Monthly (USD) |
|---|---|
| Fargate (0.25 vCPU + 0.5 GB) × 3 tasks | About $34 |
| Fargate (1 vCPU + 2 GB) × 1 task | About $45 |
| Fargate total (Before) | About $79 |
Consolidated onto 2 × t3a.small 3-year RIs (After) | About $17 |
| Savings | About -$62/month (-78%) |
The workload itself didn't change at all; simply moving it from Fargate to EC2 on 3-year RIs cut nearly 80% of the monthly cost. The biggest lesson from this was that "the extra 'convenience fee' we pay for Fargate being managed was much larger than I had imagined". If you have spare RIs, just moving your Fargate tasks onto them works better than you'd expect.
As a side benefit, the 1 vCPU + 2 GB task effectively got a spec bump by landing on a t3a.small (2 vCPU + 2 GiB). Fargate is a billing model that allocates exactly the vCPU / memory you request, while ECS tasks on EC2 can use up whatever the host has free ── a subtle difference that matters more than you'd think when reasoning about pricing.
Gotchas
- Designing the ECS capacity provider: to use EC2 as a capacity provider, you need to provision instances with an ASG and link it to ECS. It's not like Fargate, where you just declare the number of tasks.
- Not enough free EC2 capacity during rolling deploys: if you pack tasks onto EC2 sized exactly to their requested specs, there's no room for the new tasks launched during a deployment, and the deployment gets stuck. We worked around this by temporarily scaling out the ASG before deploying to stand up a "spare EC2 for deployment" in advance. The proper approach is a design that places multiple small tasks on a larger EC2 instance (leaving headroom on each host). "Tight packing" to match your RI inventory comes back to bite you in the form of more complex deployments.
- EC2 maintenance is back on your plate: OS patches, AMI updates, ECS Agent version management, and other operational work come back. You give up the main advantage of Fargate (being managed).
10. Lessons ─ What we learned by doing it
- The February 2024 Public IPv4 charge quietly adds up. It's $3.65/month per IP. Scattered across ALBs / NATs / ECS tasks / EIPs, it can balloon into double-digit dollars before you notice. Make a habit of asking "does this really need a public IP?" at design time.
- The "convenience fee" for managed services is bigger than you think. NAT Gateway and Fargate are certainly convenient, but in our case NAT Gateway → FCK-NAT freed up $99/month and Fargate → EC2 (RIs) $62/month. Decide workload by workload on your SLA requirements and your willingness to pay the "convenience fee". For internal and staging systems, dropping managed services doesn't hurt.
- Clean up the leftovers of past decisions. RIs left over after migrating from EC2 to Fargate, ALBs split up per service, WAFs set up separately for each ALB ── "legacy from past migrations and setups" lingers as quiet fixed costs. Cleaning up often pays off more than new optimizations.
- Working alongside AI lets you "learn while doing". From reading the bill to choosing how to cut costs, identifying unused services, and working around problems we hit, we consulted AI at every step of this project. Even for "costs I hadn't noticed" and "services I'd never touched", I could look into pricing, alternatives, and gotchas all at once, and as a result I was able to learn across the board while getting hands-on work done ── that was the most rewarding part of this project for me.
11. What we plan to tackle next
Here are the items we plan to continue working on as of the time of writing (2026-05). Feel free to use them as a checklist if you're looking to improve an AWS environment of a similar size.
Priority items planned for the near term
Completing the AL2023 migration ─ removing the parallel EC2 instances / EIPs
We're currently in the middle of rehosting from AL2 to AL2023, and because the old and new servers are running in parallel, we have extra EC2 instances and EIPs. Once the migration is complete and we stop and then delete the old side, we expect fixed costs to drop naturally by a few tens of dollars a month. The Before/After figures in this article are conservative values that include this migration overhead.
ECS Fargate cost optimization: Savings Plans vs. further use of EC2 RIs (ECS on EC2)
In tactic #8, we kept it to a limited rollout, moving 4 tasks onto the spare RIs, but for moobon alone, Fargate charges are still around $263/month (including recent growth, ECS alone is a third of the total). For the remaining production tasks, we're comparing the following 2 options.
| Option | Overview | Expected savings | Operational burden |
|---|---|---|---|
| Option 1 Compute Savings Plans | Buy a 1- or 3-year Compute SP covering Fargate / EC2 / Lambda. The hourly rate drops according to your commitment. | Up to 30–50% (with 3y All Upfront) | Low (just a purchase; no infrastructure changes) |
| Option 2 EC2 RI + ECS on EC2 | Move production tasks to ECS on EC2 and buy EC2 Reserved Instances. Combine with a Spot capacity provider. | Up to 40–70% (3y, stable workloads) | Medium (ASG / capacity provider design, ongoing AMI updates) |
Our tentative plan for now is a hybrid: "cover the stable baseline workload with EC2 RIs, and keep spikes and batch jobs on Fargate (on-demand + Spot)". Once the baseline is finalized, we're considering layering a 1-year No Upfront Compute SP on top to "cover whatever on-demand usage remains". Once we've done it, we'll report the results in a separate article.
Afterword
At its root, this article carries a simple message: cut out the waste in AWS and keep your operations clean. AWS is convenient, but precisely because of that, if you use it without paying attention, fixed costs keep quietly swelling. For us in Japan, cutting wasteful spending also helps shrink the country's digital trade deficit ── a small way of pushing back against the weak yen.
This whole article started with having AI read our AWS bill and point out the waste. So that anyone can try the same approach, MOOBON offers an AWS cost analysis tool. If you want to know how much "invisible waste" is in your own environment, give it a run. For cost assessments or architecture reviews, feel free to reach out at info@moobon.jp.
We plan to update this article as we make progress on the AL2023 migration, Compute Savings Plans, RDS RIs, and more.
Frequently Asked Questions
QHow does availability differ between NAT Gateway and an EC2 NAT (FCK-NAT)?
The AWS-managed NAT Gateway is redundant within each AZ and backed by a 99.99% availability SLA. An EC2 NAT (including FCK-NAT), on the other hand, goes down during an AZ failure if it runs as a single instance, so the standard approach is to ensure redundancy with an Auto Scaling Group and health checks that recover it automatically. Depending on your traffic volume and how much downtime you can tolerate, you may still decide to stick with NAT Gateway for mission-critical workloads.
QWhen did AWS start charging for Public IPv4 addresses?
Since February 1, 2024, every Public IPv4 address is billed at $0.005/hour (roughly $3.6/month). This applies not just to EC2 but to every resource with a public IP, including ALB nodes, NAT Gateways, and VPC endpoints. We recommend checking the actual numbers with the "PublicIPv4" filter in AWS Cost Explorer and listing the places where you can cut back.
QWhat are the benefits of moving from ECS Fargate back to EC2 (ECS on EC2)?
If you hold EC2 Reserved Instances, you can reduce Fargate on-demand costs by putting their unused capacity to work for ECS tasks. In the example in this article, we put staging on Fargate Spot and moved part of production onto spare EC2 RI capacity, saving about $25/month. However, it adds operational overhead, such as AMI updates, capacity planning, and differences in scaling behavior, so you need to think carefully about how far to go.
QWhat are the risks of consolidating into a single ALB?
The main considerations are the listener rule limit (100 by default, which can be raised to 200 via a support request), the number of SSL certificates (up to 25 per ALB), and separation of responsibilities between multiple teams. In this article we consolidated into 23 rules; if the count grows beyond that, we would consider splitting ALBs or adding path-based routing via CloudFront.
QHow long did the cost reduction work take?
About 3 months in total from design to execution, or roughly 5 business days of actual working time. What took the longest was the groundwork (investigating the blast radius, coordinating with stakeholders, and securing safe cutover windows); the hands-on work itself took anywhere from an hour to a few hours per resource. It's important to verify the impact against the monthly bill and to pull out early from measures that aren't paying off.
