This article was originally published at sivaro.in
How to Design Cost Efficient Architecture on AWS
Most AWS bills are a design failure, not a usage problem.
I've been doing this since 2018. At SIVARO we've built systems processing 200K events/sec, and I've watched teams burn $40K/month on architecture that should've cost $6K. The fix is almost never "buy a Savings Plan." The fix is admitting you designed for the wrong axis.
That's the core of how to design cost efficient architecture on AWS: cost is a design constraint you enforce at draw time, not a line item you reconcile at month-end. If cost isn't in the design doc, it's not in the system.
Let me show you how my thinking changed, then give you the actual comparison tables you need to make a decision.
The three cost levers nobody pulls in the right order
Everyone talks compute. Compute is the least interesting lever.
The 80/20 of AWS spend:
| Lever | Typical % of bill | Typical optimization | Effort |
|---|---|---|---|
| Data transfer + egress | 15-25% | 40-70% reduction | High (architectural) |
| Compute | 40-55% | 30-60% reduction | Medium |
| Storage + requests | 10-20% | 50-80% reduction | Low |
| Observability | 5-15% | 60-90% reduction | Low |
Most teams attack compute first because it's visible. That's backwards. In 2023 we cut a client's $18K/month bill to $7.2K, and only $1.5K of that came from compute. $5K came from killing cross-AZ chatter. $3K came from log retention. $1.3K came from S3 lifecycle.
Cross-AZ traffic costs $0.01/GB each direction. Sounds trivial. One chatty service calling another across three AZs at 50 API calls/sec, 4KB each, is 17TB/month. That's $340/month for one service pair. Multiply by 20 service pairs. Now you understand the $5K.
Compute: EC2 vs Fargate vs Lambda vs Graviton
This is where the buying-guide framing actually matters. There's no universal winner. There's a winner per workload shape.
| Option | Best for | Worst for | $/vCPU-hr effective |
|---|---|---|---|
| Lambda | Bursty, <15min, event-driven | Steady high-throughput | ~$0.0000133/GB-s |
| Fargate | Containerized, moderate traffic | Long-running steady load | ~$0.04046/vCPU-hr |
| EC2 On-Demand | Unpredictable, short-lived | Anything >30 days running | $0.0416/vCPU-hr (m6i.large class) |
| EC2 + Savings Plan | Steady state >1yr | Rapidly changing instance mix | down to ~$0.026/vCPU-hr |
| Graviton (c7g) | Linux workloads, ARM-ready | x86-only binaries | ~20% cheaper than comparable x86 |
The contrarian take: Lambda is expensive at scale and cheap at sparsity, and teams get this exactly backwards. I've seen a 40 rps API on Lambda run $9K/month that would've cost $1.4K on two Fargate tasks. Conversely, an internal tool that runs 200 times a day on Fargate burns $28/month to do nothing.
The break-even is roughly 15-20 sustained requests/second per function. Below that, Lambda wins. Above, containers win. I've written about this pattern repeatedly and the math hasn't changed much since Graviton pricing dropped.
Graviton is the single easiest 20% you'll ever take. We migrated 90% of our client fleet to Graviton4 (c8g/r8g) in early 2026. Node.js, Go, Python, Java — all fine. Postgres on r8g beat r6i by 22% on identical workloads. The only thing that broke: a legacy Fortran numeric library and one proprietary x86-only vendor SDK.
# Don't trust marketing benchmarks. Measure your own workload.
import boto3, time
from datetime import datetime, timedelta
ce = boto3.client('ce', region_name='us-east-1')
resp = ce.get_cost_and_usage(
TimePeriod={
'Start': (datetime.utcnow() - timedelta(days=30)).strftime('%Y-%m-%d'),
'End': datetime.utcnow().strftime('%Y-%m-%d')
},
Granularity='DAILY',
Metrics=['UnblendedCost', 'UsageQuantity'],
GroupBy=[
{'Type': 'DIMENSION', 'Key': 'INSTANCE_TYPE'},
{'Type': 'DIMENSION', 'Key': 'USAGE_TYPE_GROUP'}
]
)
for day in resp['ResultsByTime']:
for group in day['Groups']:
itype, utype = group['Keys']
cost = float(group['Metrics']['UnblendedCost']['Amount'])
if cost > 50: # only flag meaningful spend
print(f"{day['TimePeriod']['Start']} | {itype} | {utype}: ${cost:.2f}")
The storage decision tree, compressed
S3 looks simple. It isn't. The difference between a well-designed bucket and a default bucket is 5-10x in cost.
| Class | Retrieval | Storage $/TB/mo | Use when |
|---|---|---|---|
| S3 Standard | ms | $23 | Hot data, <30 days |
| S3 Standard-IA | ms | $12.50 | Access <1x/month |
| S3 Glacier Instant | ms | $4 | Compliance, rare reads |
| S3 Glacier Flexible | min-hrs | $3.60 | Backups, DR |
| S3 Glacier Deep Archive | 12hrs | $0.99 | 7-year retention |
The trap: minimum storage durations. Standard-IA charges 30 days minimum. Glacier Instant is 90 days. Deep Archive is 180 days. Delete early and you pay the full minimum anyway. I've watched a team "optimize" logs into IA and pay more because they rotate every 14 days.
Lifecycle rules that actually pay for themselves:
{
"Rules": [
{
"ID": "logs-tiering",
"Filter": {"Prefix": "logs/"},
"Status": "Enabled",
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER_IR"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"}
],
"Expiration": {"Days": 2555},
"NoncurrentVersionExpiration": {"NoncurrentDays": 30}
}
]
}
Note the 2555 days — that's 7 years, which for many regulated industries is the actual retention floor. Don't guess; check with legal before you set expiration.
The observability tax — the line item that grew 400% and nobody noticed
Datadog, CloudWatch, Grafana Cloud. Pick your poison. All of them will quietly become your 3rd largest line item if you don't instrument your instrumentation.
In August 2026 I audited a client paying $14,200/month to Datadog. Their actual necessary observability was about $2,800/month. The other $11,400 was:
- Log ingestion of debug-level logs in prod ($4,100)
- Custom metrics with 10-second resolution that nobody queried ($3,900)
- APM tracing at 100% sampling on a high-throughput service ($2,400)
- 40 unused dashboards still ingesting ($1,000)
The fix took two weeks and cut 80%. Not because the tools are bad. Because they default to maximum verbosity.
CloudWatch Logs specifically: $0.50/GB ingested, $0.03/GB stored. The ingress is 16x the storage. That's where the money is. Ship logs to S3 directly if you don't need Logs Insights. Use subscription filters to route only errors to CloudWatch and everything to S3.
# CloudWatch log group with sane retention — saves 90% on storage
aws logs put-retention-policy \
--log-group-name /aws/lambda/my-function \
--retention-in-days 14
# Better: ship to S3 for long-term, keep 7-day CloudWatch for hot queries
aws logs put-subscription-filter \
--log-group-name /aws/lambda/my-function \
--filter-name to-firehose \
--filter-pattern "" \
--destination-arn arn:aws:firehose:us-east-1:123456789012:deliverystream/logs-to-s3
Data transfer: the silent killer
The single biggest mistake I see: services chattering cross-AZ because someone forgot that "us-east-1" is actually six availability zones, each a separate data center with metered interconnects.
Costs (2026, us-east-1):
- Same AZ, private IP: free
- Cross-AZ, same region: $0.01/GB each direction
- Cross-region: $0.02/GB
- Internet egress: $0.09/GB (first 10TB)
That internet egress number is why putting CloudFront in front of S3 cuts a video-heavy bill by 60-80%. CloudFront egress to internet is $0.085/GB, and origin fetch from S3 to CloudFront is free. Plus caching means repeat fetches don't hit origin at all.
VPC endpoints are the other big win. Every NAT Gateway call to S3 or DynamoDB is $0.045/GB processed plus $0.045/hr for the NAT. A Gateway VPC endpoint for S3 and DynamoDB is free. Free. The only reason people don't use them is they don't know they exist.
# Gateway endpoints — free, kill your NAT bill for AWS service traffic
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
tags = { Name = "s3-gateway-endpoint" }
}
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
tags = { Name = "dynamodb-gateway-endpoint" }
}
One client had $6,800/month in NAT Gateway data processing. Gateway VPC endpoints dropped it to $900. Nothing else changed. That's a 4-week engineering project paying for itself monthly forever.
Databases: the choice that compounds
RDS vs Aurora vs DynamoDB vs self-managed on EC2.
| Option | Sweet spot | Trap |
|---|---|---|
| RDS Postgres | <2TB, predictable load | Multi-AZ doubles cost |
| Aurora Serverless v2 | Spiky, dev/test | ACU floor can idle at $44/month |
| Aurora Provisioned | Sustained, read-heavy | I/O charges bite on hot tables |
| DynamoDB On-Demand | Unpredictable, spiky | 5x more per request than provisioned |
| DynamoDB Provisioned + auto-scaling | Predictable, high-volume | Capacity planning still exists |
| Self-managed on EC2 | Custom needs, cost extremes | You own the pager |
DynamoDB is fascinating because most teams pick the wrong billing mode and never revisit it. On-Demand sounds safer. It is. It's also 5x more per request. If your traffic is predictable — even weekly — provisioned + auto-scaling is dramatically cheaper.
Aurora Serverless v2 has an ACU floor of 0.5 and scales to 128. The floor means you pay $43.80/month minimum per cluster in us-east-1 (0.5 ACU × $0.12/ACU-hr × 730). For dev environments that's fine. For 15 dev environments it's $660/month of nothing.
The contrarian take here: for many workloads under 500GB, RDS on Graviton with reserved instances beats Aurora on cost by 40% and matches it on performance. Aurora's value is at scale and for its read replica fan-out. If you're running 200GB with modest read traffic, you're paying Aurora tax for features you don't use.
Reserved capacity, Savings Plans, and the commitment game
The single most reliable 30-40% is commitment-based pricing. But you have to commit correctly.
| Instrument | Discount | Flexibility | Commitment |
|---|---|---|---|
| Compute Savings Plan | ~17-30% | Any instance family/region | 1 or 3 yr, $/hr |
| EC2 Instance Savings Plan | ~30-40% | Family-locked, region-locked | 1 or 3 yr, $/hr |
| Reserved Instance (Standard) | ~30-45% | Family, region, OS locked | 1 or 3 yr |
| Reserved Instance (Convertible) | ~25-35% | Can exchange family | 1 or 3 yr |
My rule: Compute Savings Plans for the low-confidence baseline, EC2 Instance Savings Plans for the high-confidence baseline. Never buy RIs anymore unless you're on a legacy account with specific needs. The 3-year amortization math for Savings Plans is close enough to RIs that flexibility wins.
The commitment sizing rule: commit to your p10 (10th percentile) usage, not your average. I've watched teams commit to average and pay for unused capacity 4 months a year. Commit low, cover the spike with on-demand or spot.
Spot instances are a completely different animal. Up to 90% off on-demand, but with a 2-minute termination notice (or 30-second, if you opt for that). Works for: batch, stateless workers, CI runners, ML training with checkpointing. Fails for: anything maintaining long connections or in-memory state without checkpointing.
Well-Architected reviews and Cost Anomaly Detection
AWS's Well-Architected Framework has a Cost Optimization pillar. It's worth running once. But the real tool is Cost Anomaly Detection, and most teams don't have it configured.
import boto3
ce = boto3.client('ce', region_name='us-east-1')
ce.create_anomaly_monitor(
AnomalyMonitorName='AccountWideMonitor',
MonitorType='DIMENSIONAL',
MonitorDimension='SERVICE'
)
ce.create_anomaly_subscription(
SubscriptionName='DailyAnomalyAlert',
MonitorArnList=['arn:aws:ce::123456789012:anomalymonitor/abc-123'],
Frequency='DAILY',
Subscribers=[
{'Address': 'ops@yourcompany.com', 'Type': 'EMAIL'}
],
Threshold=50 # alert on $50+ anomalies
)
Configure this in the first week of any new account. It has caught three major leaks for us in 2026 alone — one was a runaway Lambda from a recursive trigger that would've been $12K/month. We caught it at $340.
Also enable Cost Allocation Tags at the org level. Untagged resources are unmanageable resources. I enforce tagging with an SCP that denies resource creation without CostCenter, Owner, and Environment tags. Sounds harsh. It's the only thing that works.
FAQ
Doesn't cost optimization mean worse performance?
Usually the opposite. The optimizations that matter — killing chatty RPCs, right-sizing instances, using Graviton, adding caching — improve latency. The optimizations that hurt performance are things like running at 95% CPU to save $200/month. Never do that.
What's the single biggest change I can make this week?
Gateway VPC endpoints for S3 and DynamoDB. Free to set up, no performance cost, and if you have any meaningful S3/DynamoDB traffic going through NAT, the savings are immediate.
Is Savings Plans worth the lock-in risk?
For steady state, yes, always. For a startup with five months of runway and pivot risk, commit to nothing and eat the on-demand premium. The premium is insurance.
How do I know if I should use Lambda or containers?
Measure requests per second. If sustained peak is under 15 rps per function endpoint, Lambda. Above 40 sustained, containers. Between, benchmark both on your actual workload for one week. I've seen it go either way in that band.
Aurora or RDS?
Under 500GB with modest read traffic, RDS on Graviton with reserved instances. Above 1TB with heavy read fan-out, Aurora. The middle is genuinely ambiguous — test.
How much should observability cost?
Target 5-8% of total cloud spend. If it's over 12%, you have a configuration problem, not a usage problem. Most of ours comes from log retention and trace sampling rates.
Does multi-region always double cost?
No. Active-passive with warm standby is typically 30-40% more than single region, not 100%. Active-active is 2x+ and generally only justified when you have hard RTO/RPO requirements or a global user base.
What about commitments on new services?
Be very careful. New services in preview or with evolving pricing models — like the 2025 Bedrock model changes — are the wrong place for 3-year commitments. Commit to the boring infrastructure, buy evolution on-demand.
The architecture review that pays for itself
Here's the exercise I run with every SIVARO client in the first two weeks. Do it yourself if you want.
- Pull 90 days of Cost Explorer data grouped by service
- For each service over $1K/month, identify the top 3 resource-level contributors
- For each top resource, ask: is this designed for the correct cost axis? (durability, latency, throughput, cost)
- Itemize the delta between current and "designed for cost" state
- Rank by savings-per-engineering-hour
I've never run this on a bill over $10K/month and found less than 35% removable. Not with toys. With real, production-safe changes that ship over 6-12 weeks.
The trick in how to design cost efficient architecture on AWS isn't knowing every service's pricing table. It's asking, at design time, "what does this decision cost per unit of work delivered" — and refusing to ship designs that can't answer that question.
Cost efficiency is a design culture, not a ticket you close.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.
Top comments (0)