DynamoDB Cost Optimization: The 5 Levers That Actually Matter
DynamoDB pricing is straightforward - you pay for reads, writes, and storage. But the amount you pay for each operation varies dramatically based on how you configure your table and how you design your queries.
These are the five levers that make the biggest difference, in order of impact.
1. Switch from on-demand to provisioned (with auto-scaling)
This is the single biggest cost lever. On-demand pricing is convenient but expensive - roughly 6.5x more per read and 5x more per write compared to provisioned capacity.
| Mode | Read cost (per million) | Write cost (per million) |
|---|---|---|
| On-demand | $1.25 | $6.25 |
| Provisioned | $0.19 | $1.18 |
The catch: provisioned requires you to predict capacity. If you under-provision, requests get throttled. The solution is auto-scaling - DynamoDB adjusts provisioned capacity based on actual usage, with a target utilization you set (typically 70%).
Switch once you have 2 weeks of stable CloudWatch metrics showing predictable traffic patterns. Spiky, unpredictable workloads should stay on-demand.
For a table doing 1,000 reads/second and 200 writes/second consistently:
- On-demand: ~$7,900/month
- Provisioned with auto-scaling: ~$1,200/month
- Savings: ~85%
Start on-demand for development and early production. Switch to provisioned once traffic stabilizes.
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.
2. Use eventually consistent reads (they’re 50% cheaper)
Every DynamoDB read is eventually consistent by default. Strongly consistent reads cost twice as much (1 RCU per 4KB vs 0.5 RCU per 4KB).
Most applications don’t need strong consistency. If a user creates a record and reads it back 50ms later, eventually consistent reads will return the latest data in almost every case. DynamoDB’s replication typically completes within single-digit milliseconds.
You need strong consistency for: financial transactions where stale reads could cause incorrect balances, inventory systems where overselling is a risk, and distributed locks.
You don’t need it for: user profiles, dashboards, content displays, search results, analytics, activity feeds. Basically everything that tolerates a sub-second replication lag.
Check your codebase for ConsistentRead: true. Every one you can remove saves 50% on that read’s cost.
3. Project only the attributes you need
When you Query or Scan without a projection expression, DynamoDB returns every attribute on every item. You’re charged for the full item size in RCUs, even if you only need two fields.
// Expensive: reads full 2KB item, costs 1 RCU per 2 items
const result = await OrderEntity.query.byCustomer({ customerId }).go();
// Cheaper: reads only orderId and total, ~100 bytes per item
const result = await OrderEntity.query.byCustomer({ customerId })
.go({ attributes: ["orderId", "total", "status"] });
For a table where items average 2KB but you only need 200 bytes per item, projections reduce read costs by 90%.
This also applies to GSIs. Use KEYS_ONLY or INCLUDE projection types instead of ALL when you don’t need every attribute in the GSI. KEYS_ONLY GSIs are dramatically cheaper to maintain - smaller storage and fewer write units on each update. Related: treating GSIs as relational indexes is one of the most common single-table design mistakes - overloading GSIs cuts write amplification further.
4. Fix your key design (stop scanning)
The most expensive DynamoDB operation is a Scan. It reads every item in the table and charges you for all of it.
But the more insidious cost problem is broad queries - Queries with a PK match but a SK prefix that’s too wide, returning thousands of items when you only need 10.
// Broad: reads all 50,000 items in the partition
Query(pk=TENANT#t_01)
// Tight: reads only the ~200 project items
Query(pk=TENANT#t_01, sk begins_with PROJECT#)
// Tightest: reads only projects from this month
Query(pk=TENANT#t_01, sk BETWEEN PROJECT#2026-04-01 AND PROJECT#2026-04-30)
If you’re using FilterExpression to narrow results after a broad Query, you’re paying for every item read before the filter is applied. Filters reduce bandwidth but not RCU cost.
Look at your queries in CloudWatch. Sort by consumed capacity. The queries consuming the most RCUs are the ones where key design improvements will have the biggest impact. The Schema Migrations guide covers how to restructure keys on existing tables.
Production issue
The longer this runs in production, the harder it is to fix.
DynamoDB schemas harden over time. A review now is hours of work. A migration later is weeks. Async, fixed price, 5 business days.
5. Use TTL to auto-delete stale data
Every gigabyte stored in DynamoDB costs $0.25/month. That adds up when you’re storing data nobody reads.
DynamoDB TTL deletes items automatically after a timestamp you set - at zero cost. No Lambda. No cron job. No additional write charges.
Common candidates for TTL:
- Session tokens (expire after 24 hours)
- Temporary invites (expire after 7 days)
- Raw IoT readings (expire after 30 days, replaced by aggregations)
- Activity feed items older than 90 days
- Soft-deleted records waiting for permanent removal
Set the ttl attribute to a Unix epoch timestamp. DynamoDB checks it continuously and deletes expired items within 48 hours.
const THIRTY_DAYS = 30 * 24 * 60 * 60;
await ReadingEntity.put({
deviceId: "sensor_01",
timestamp: new Date().toISOString(),
temperature: 22.4,
ttl: Math.floor(Date.now() / 1000) + THIRTY_DAYS,
}).go();
The cost audit checklist
Before your next DynamoDB bill review:
- Capacity mode - Are you on on-demand with stable traffic? Switch to provisioned.
- Consistent reads - Search for
ConsistentRead: true. Remove where unnecessary. - Projections - Are you returning full items when you only need a few fields?
- Broad queries - Sort CloudWatch metrics by consumed capacity. Which queries are too wide?
- Storage - Is stale data accumulating? Add TTL.
- GSI projection types - Are your GSIs projecting
ALLwhenKEYS_ONLYwould suffice? - GSI count - Can any GSIs be overloaded to reduce write amplification?
Most DynamoDB tables can reduce costs by 40-70% by addressing these seven items without changing application logic.
DynamoDB cost is largely determined by schema design. The patterns on singletable.dev bake in cost efficiency - tight SK prefixes, GSI overloading (see the SaaS Multi-Tenant pattern), and TTL where appropriate (IoT Time-Series and Analytics Events both rely on it). I’m building singletable.dev to make cost-aware schema design visual.