DynamoDB On-Demand vs Provisioned: The Math, Not the Marketing
AWS markets on-demand DynamoDB as the easy default - no capacity planning, scales instantly, “just works.” It’s true. It’s also significantly more expensive than provisioned capacity for any predictable workload, by a factor that surprises most teams when they finally do the math.
This post is the actual math. The trade-offs nobody tells you. The decision rule for which mode to start with, when to switch, and the rare cases where provisioned is the wrong call even at scale.
The short version: on-demand is right for unpredictable workloads and small tables; provisioned with auto-scaling is right for predictable workloads at any scale. Most production tables eventually qualify for the second. Most teams never make the switch.
The pricing, side by side
US-East-1, standard table class, current pricing:
| On-Demand | Provisioned | |
|---|---|---|
| Read (per million RCUs) | $0.125 | n/a |
| Write (per million WCUs) | $0.625 | n/a |
| Read capacity (per RCU-month) | n/a | ~$0.0001625/hour ≈ $0.118/month |
| Write capacity (per WCU-month) | n/a | ~$0.000813/hour ≈ $0.594/month |
Translating to comparable units (cost per million ops, assuming 100% utilization on provisioned):
| On-Demand per million | Provisioned per million (100% util) | |
|---|---|---|
| Read | $0.125 | ~$0.0193 |
| Write | $0.625 | ~$0.118 |
On-demand is roughly 6.5x more per read and 5.3x more per write. That’s the headline. The catch: provisioned only hits that low cost at 100% utilization, which is impossible to maintain. Realistic auto-scaling targets ~70% utilization, so the effective cost is closer to:
| On-Demand | Provisioned (70% util) | |
|---|---|---|
| Read per million | $0.125 | ~$0.0276 |
| Write per million | $0.625 | ~$0.169 |
Provisioned is still ~4.5x cheaper for reads, ~3.7x cheaper for writes, at realistic utilization. Reserved capacity (annual commitment) drops it further.
A concrete table
A table doing 1,000 reads/sec and 200 writes/sec sustained, 24/7:
- Reads/month: 1,000 × 86,400 × 30.4 = ~2.6 billion
- Writes/month: 200 × 86,400 × 30.4 = ~525 million
Costs (storage and other costs excluded for clarity):
| On-Demand | Provisioned (70% util) | |
|---|---|---|
| Reads | $328 | $72 |
| Writes | $328 | $89 |
| Total | $656/month | $161/month |
That’s a 4x difference. Annualized: $7,900 vs $1,930. The savings are real money once your traffic stabilizes.
At 10x that scale (10,000 reads/sec, 2,000 writes/sec), the gap is $6,560/mo vs $1,610/mo. At 100x: $65,600/mo vs $16,100/mo. The percentage stays the same; the absolute dollars get serious.
Production issue
You're likely losing money on this in production.
A wrong partition key or missing GSI is a live cost problem. Get a DynamoDB schema review before your next deploy — async, fixed price, 5 business days.
When on-demand is the right call
On-demand isn’t just “the easier option.” For some workloads, it’s actually the right call - including some at significant scale.
Spiky workloads with high peak-to-average ratios: if your traffic peaks at 10x the average, you have to provision for the peak (otherwise throttling) but you’re paying for that capacity 24/7. On-demand pays only for the spikes. For workloads where peak/average > 5x, on-demand is often cheaper.
Unpredictable launch traffic: when a feature ships and you don’t know if it’ll get 10 users or 10 million, on-demand absorbs whatever happens. You can switch to provisioned later once traffic stabilizes.
Dev / staging / non-production tables: these don’t run 24/7, traffic is low, and you don’t want to think about capacity. On-demand. Always.
Small tables (< $20/month either way): the dollar difference is rounding error. Optimize for engineering simplicity.
Workloads where you can’t predict capacity: customer-facing APIs whose load depends on customer behavior, third-party integrations that fire unpredictably, batch jobs that run on irregular schedules.
When provisioned is the right call
Predictable, sustained workloads: if your CloudWatch shows a consistent 1,000 reads/sec ± 30% for the past two weeks, you’re a candidate. Auto-scaling handles the variance; you pay for the baseline.
Anything at meaningful scale: anything spending more than ~$200/month on DynamoDB on-demand should be evaluated. If you can save $100+/month for a few hours of switching work, do it.
Workloads with known peak hours: auto-scaling can ramp up before peak and ramp down after. You’re paying provisioned rates during the peak (cheap) and a smaller provisioned baseline during the trough.
Reserved capacity (annual commitment): if you have steady traffic, you can buy reserved capacity for a 1-year or 3-year term and cut costs another 50-77%. Only an option on provisioned. For tables you’re confident about, this is the cheapest mode AWS offers.
The auto-scaling caveats
Provisioned with auto-scaling sounds magical: capacity adjusts automatically, you don’t have to think about it. The reality is more nuanced.
Auto-scaling is slow. It reacts in minutes, not seconds. A traffic spike that lasts 30 seconds will throttle even with auto-scaling enabled, because the scaling didn’t have time to kick in. Provision for spikes that are too short to scale into, or accept the throttling.
The target utilization is not a maximum. Setting target to 70% means “try to keep utilization at 70%” - it does NOT mean “scale up before hitting 70%.” Sustained traffic at 90% will hit your provisioned ceiling before scaling reacts.
There’s a minimum and maximum you set. Auto-scaling can’t go below your floor (so you pay for the floor at all times) or above your ceiling (so a real spike still throttles). Picking these well requires actually looking at the metrics.
Scale-down is conservative. Scale-up triggers fast; scale-down waits to be sure the lower demand is sustained. That conservatism costs you - you’ll pay for the post-spike capacity for longer than you used it.
In practice, auto-scaling cuts your bill significantly versus over-provisioning, but it’s not a silver bullet. You still need to look at metrics and tune the floor/ceiling.
The “switch from on-demand to provisioned” workflow
When a table outgrows on-demand pricing, the migration is:
- Watch CloudWatch for 2-4 weeks. Note the P50, P95, P99 of consumed RCUs/WCUs. The workload should look stable - no major shifts in usage patterns.
- Pick a provisioned floor and ceiling. Floor ≈ P50 of consumed capacity. Ceiling ≈ 1.5x of P99. This buys you headroom for spikes while not over-provisioning the baseline.
- Set target utilization to 70%. Lower = more headroom but more cost. 70% is the AWS default and a reasonable starting point.
- Switch the table to provisioned. This is a single API call (or a Terraform / CDK change). No downtime, no migration.
- Watch for throttling. Set CloudWatch alarms on
WriteThrottleEventsandReadThrottleEvents. If you see throttling, raise the ceiling or lower the target utilization.
Cost-wise, you’ll usually see savings within the first day. The hard part is the 2-4 weeks of patience to make sure the workload is actually predictable.
You can switch back to on-demand if the workload changes - but DynamoDB enforces a cooldown period (24 hours after switching, you can’t switch back). Don’t flap.
Reserved capacity (the deeper cut)
If your workload is steady and you’re already on provisioned, reserved capacity is the next lever. AWS sells RCU/WCU reservations at a 53% discount for 1-year and 77% for 3-year commitments.
The math:
- Provisioned (no reservation): $0.118/RCU-month, $0.594/WCU-month
- Reserved 1-year: ~$0.055/RCU-month, ~$0.279/WCU-month
- Reserved 3-year: ~$0.027/RCU-month, ~$0.137/WCU-month
For the 1,000 RCU + 200 WCU table from earlier, 1-year reserved cuts the monthly bill from $161 to $75. 3-year cuts it to $37.
The catch: you commit to paying for that capacity whether you use it or not. Right-size it. Buy reservations for capacity you’re 100% sure you’ll use; let auto-scaling handle the rest at on-demand-style billing within the provisioned envelope.
For most teams, reserved capacity is leaving money on the table. It’s the easiest cost win for tables doing more than ~$1,000/month.
Storage cost (often forgotten)
Both modes charge identically for storage: $0.25/GB-month for the standard table class, $0.10/GB-month for the infrequent-access class.
Storage isn’t usually the largest cost in a DynamoDB bill, but it can be for tables with lots of cold data. If you have a table with 500GB of historical data that’s rarely read, you’re paying $125/month for storage regardless of which capacity mode you’re on. Mitigations:
- Switch to the IA table class - 60% storage cost reduction for read-rare data, slightly higher per-request cost. Net savings if your read rate is low.
- Use TTL to expire stale data - see TTL patterns for the playbook.
- Archive to S3 via Streams - delete from DynamoDB once moved.
Decision matrix
| Workload | Choose | Why |
|---|---|---|
| Brand new feature, no traffic data | On-demand | You don’t know the shape yet |
| Dev / staging / sandbox | On-demand | Low traffic, low total cost, simplicity wins |
| < $50/month either way | On-demand | Optimize for engineering time |
| Stable production with > 2 weeks of metrics | Provisioned + auto-scaling | 3-5x cheaper |
| Highly spiky (peak/avg > 5x) | On-demand | Provisioned would over-provision the trough |
| 24/7 steady high traffic | Provisioned + reserved capacity | 5-10x cheaper |
| Mixed: stable base + occasional spikes | Provisioned + auto-scaling with high ceiling | Auto-scaling absorbs reasonable spikes |
| You’re a startup with < $1k/mo total AWS bill | On-demand for everything | Engineering hours cost more than the difference |
What teams actually do wrong
Stay on on-demand forever. “It works, why change it” - while paying 4x. The team rationalizes it as “we don’t have engineering time to do capacity planning,” which is reasonable for a $50/month table and absurd for a $5,000/month one.
Switch to provisioned with no auto-scaling. Now they’re throttling under spikes and overprovisioning during troughs. The middle of nowhere.
Set auto-scaling ceilings too low. Real spikes throttle. They blame “DynamoDB” instead of their ceiling.
Buy reserved capacity for unpredictable workloads. They commit to capacity that ends up unused. The capacity expires worthless. The worst-case for reserved capacity is worse than not using it at all.
Forget the table class option. They have 500GB of cold data on the standard class. Switching to IA saves money immediately with minimal downside.
My take
For most teams, the right path is:
- Start every new table on on-demand. Engineering simplicity wins until you have data.
- Watch your DynamoDB bill monthly. Sort tables by spend. Anything > $200/month is a candidate for review.
- Switch the top spenders to provisioned + auto-scaling once they have 2-4 weeks of stable metrics. Floor at P50 of usage, ceiling at 1.5x of P99, target 70% utilization.
- For the very top spenders, layer reserved capacity on the predictable baseline.
- Revisit twice a year. Workloads shift; the optimal mode does too.
If you do this, your DynamoDB bill is somewhere between 2x and 10x cheaper than if you’d stayed on on-demand for everything, with no architectural changes - just capacity mode tuning.
This is the kind of thing that compounds. The schema decisions covered in DynamoDB cost optimization interact with capacity mode: tighter queries mean fewer RCUs consumed, which means a smaller provisioned baseline, which means a bigger reservation discount applies. Get the schema right and the capacity numbers shrink.
Capacity mode is the easiest cost lever in DynamoDB - and the most ignored. The pattern library at singletable.dev minimizes the underlying RCU/WCU consumption, which is what makes provisioned capacity cheap in the first place.
DynamoDB fundamentals covers RCUs and WCUs in depth - worth reading first if capacity units aren’t already familiar. The schema-level cost levers are in DynamoDB cost optimization.