DynamoDB vs S3 for Caching — How to Choose (2026)
aws/dynamodb-vs-s3-caching-2026 · Updated · Tags: aws, dynamodb, s3, caching, database
正在用 AI 翻譯成中文,可能需要一分鐘… 以下暫時顯示英文原文。
DynamoDB vs S3 for Caching — How to Choose (2026)
Colour key: DynamoDB S3 Hybrid
TL;DR
What they are: DynamoDB is a key-value / document database — small items, millisecond lookups, per-item expiry. S3 is an object store — files of any size, very cheap per GB, but slower per request and with no real query.
For a cache of computed results (API responses, AI output, rendered fragments) keyed by a hash: 🏆 DynamoDB wins the default case — exact-key lookup, native per-item TTL, conditional writes and atomic counters are exactly what a cache needs.
Switch to S3 when the values are big (tens of KB and up, or anything near 400 KB), when you store a lot of them for a long time, or when a browser/CDN should download the cached file directly.
| DynamoDB — pick it when | S3 — pick it when | |
|---|---|---|
| Advantage | Single-digit ms reads · per-item TTL · conditional + atomic writes · query by key range / GSI | ~11× cheaper storage per GB · no size limit worth worrying about · CDN / presigned-URL delivery · browsable as files |
| Don't use it if you need… | ❌ values over 400 KB❌ to keep many GB for months (storage is ~11× S3)❌ to serve the cached blob straight to a browser | ❌ per-item expiry at minute/hour precision❌ to query the cache (only prefix listing)❌ counters, or many tiny high-frequency writes |
Hybrid: keep the key, metadata and TTL in DynamoDB and put the body in S3, storing the S3 key in the item. You get DynamoDB's lookup and expiry with S3's price per GB.
Summary table — the factors that decide it
Bold = winner of that row. Prices are us-east-1 list prices (on-demand DynamoDB, S3 Standard) — other regions such as Sydney are somewhat higher. Check the pricing pages before committing.
| # | Factor | DynamoDB | S3 | Winner |
|---|---|---|---|---|
| 1 | Read latency | single-digit ms | tens of ms (≈100 ms+ first byte on small objects) | DynamoDB |
| 2 | Max value size | 400 KB per item | TB-scale per object | S3 |
| 3 | Expiry | per-item TTL (epoch-seconds attribute) | lifecycle rule — per prefix/tag, whole days | DynamoDB |
| 4 | Storage cost | $0.25 / GB-month | $0.023 / GB-month | S3 (~11×) |
| 5 | Write cost | $0.625 / M write units (1 unit per 1 KB) | $5.00 / M PUTs (any size) | DynamoDB under ~8 KB · S3 above |
| 6 | Read cost | $0.125 / M read units (4 KB each; eventually consistent = half) | $0.40 / M GETs (any size) | DynamoDB under ~24 KB · S3 above |
| 7 | Querying | PK + sort-key ranges, GSIs, filters | list by prefix only | DynamoDB |
| 8 | Conditional writes | ✅ any condition expression | ✅ If-None-Match / If-Match only (since 2024) | DynamoDB |
| 9 | Atomic counters (hit counts) | ✅ UpdateItem ADD | ❌ none | DynamoDB |
| 10 | Direct delivery to clients | ❌ must go through your API | ✅ CloudFront / presigned URLs | S3 |
| 11 | Browse / inspect by hand | console item explorer | ✅ files — download, open, diff | S3 |
| 12 | Consistency | eventual by default, strong on request | strong read-after-write always | S3 |
| 13 | Throughput | on-demand scales automatically; each partition caps at 3,000 read units / 1,000 write units per second | 3,500 PUT / 5,500 GET per second per prefix, scales with prefixes | tie |
| = | Total | 5 + rows 5–6 for small values | 5 + rows 5–6 for large values | DynamoDB for small cached results |
The raw count is a draw — value size breaks it. For a cache, rows 1 and 3 (latency and per-item expiry) matter most, which is why DynamoDB is the default until values get large.
Row 5 crossover: an 8 KB write is 8 write units = $5 per million — the same as one S3 PUT. Row 6 crossover: a 24 KB eventually-consistent read is 6 × 0.5 = 3 read units = $0.375 per million, just under one S3 GET. Transactional reads/writes cost double.
Worked cost example
1 million cached results, each read once, stored for a month (us-east-1, eventually consistent reads):
| Value size | DynamoDB writes + reads + storage | S3 PUTs + GETs + storage | Cheaper |
|---|---|---|---|
| 1 KB | $0.63 + $0.06 + $0.25 = ≈ $0.94 | $5.00 + $0.40 + $0.02 = ≈ $5.42 | DynamoDB (5.8×) |
| 8 KB | $5.00 + $0.13 + $2.00 = ≈ $7.13 | $5.00 + $0.40 + $0.18 = ≈ $5.58 | S3 (1.3×) |
| 30 KB | $18.75 + $0.50 + $7.50 = ≈ $26.75 | $5.00 + $0.40 + $0.69 = ≈ $6.09 | S3 (4.4×) |
A few thousand cache entries a month cost cents on either. Choose on latency, expiry and size limit, and only let price decide once you're in the millions of items or many GB.
How to choose
Step 1 — the hard blockers. Any one decides it outright:
| # | If… | Then |
|---|---|---|
| 1 | Any value can exceed 400 KB | S3 — or Hybrid if you also need the rows below |
| 2 | Entries must expire per item (hours, not days) | DynamoDB — S3 lifecycle works in whole days per prefix |
| 3 | A browser or CDN should fetch the cached file directly | S3 + CloudFront / presigned URLs |
| 4 | You need hit counters or "write only if absent" with conditions beyond existence | DynamoDB |
Step 2 — otherwise, score your workload:
| Answer yes → | Points |
|---|---|
| Typical value is under ~8 KB | +2 DynamoDB |
| Typical value is over ~30 KB | +2 S3 |
| Cache read latency is on the user's critical path | +2 DynamoDB |
| You need to list / query entries (by user, date, type) | +2 DynamoDB |
| You'll keep many GB for months | +2 S3 |
| Entries are keyed by a hash and looked up exactly | +1 DynamoDB |
| People want to open cached output as files | +1 S3 |
| What you're caching takes seconds to produce (AI, heavy reports) | tie — either is ~1000× faster than a miss |
Per-item TTL is the feature caches need most, and it's the one S3 can't match.
Pros
| Dimension | DynamoDB | S3 |
|---|---|---|
| Lookup | ✅ GetItem by key in single-digit ms | Simple GetObject by key |
| Expiry | ✅ Per-item TTL — set an epoch-seconds attribute; deletes are free (no write units) | ✅ Lifecycle expiry is also free, set once per prefix |
| Size | Plenty for text, JSON and most API responses | ✅ No practical limit — images, PDFs, big JSON, archives |
| Price | ✅ Cheapest for small items — a 1 KB write is 8× cheaper than an S3 PUT | ✅ ~11× cheaper storage, flat price per request whatever the size |
| Writes | ✅ Rich condition expressions, atomic counters, transactions | ✅ If-None-Match for "create only if missing" |
| Queries | ✅ Sort-key ranges and GSIs — "latest 20 results for user X" in one call | — |
| Delivery | — | ✅ Serve straight to clients through CloudFront or presigned URLs — no API in the path |
| Ops | ✅ Serverless, on-demand billing, point-in-time recovery | ✅ Serverless, versioning, replication, storage classes |
| Visibility | Console item explorer | ✅ Entries are files you can download, open and diff |
Cons
| Dimension | DynamoDB | S3 |
|---|---|---|
| Size | ❌ 400 KB hard cap per item, attribute names included | — |
| Price | ❌ Billed per KB — big values get expensive fast (30 KB write = 30 units) · ❌ storage ~11× S3 | ❌ $5 per million PUTs even for a 100-byte object — expensive for many tiny writes |
| Expiry | ❌ TTL deletion is lazy — typically within a few days, and expired items still come back in reads until then, so filter on the TTL yourself | ❌ Whole days only, per prefix or tag — no "expire this one entry in 2 hours" |
| Latency | — | ❌ Tens to ~100+ ms per request — slow for a hot cache in front of a fast API |
| Queries | ❌ Access patterns must be designed up front; a Scan reads the whole table | ❌ No query — only lexicographic prefix listing |
| Counters | — | ❌ No atomic increment; read-modify-write races |
| Delivery | ❌ Every read goes through your code | — |
| Hot keys | ❌ One very hot partition key is capped (3,000 read units / 1,000 write units per second) | ❌ Per-prefix request caps; spread keys across prefixes at high rates |
| Lock-in | ❌ AWS-only API | S3 API is a de-facto standard (R2, MinIO, GCS interop) |
Similarities — stop agonising over these
- Serverless — no servers, no capacity planning on on-demand DynamoDB or S3.
- Durability — both replicate across multiple AZs in a region (except S3 One Zone / Express classes).
- IAM — the same IAM policies, KMS encryption at rest, VPC endpoints.
- Cold misses dominate — if the thing you cache takes seconds to compute, a 5 ms vs 80 ms hit is noise to the user; both are a huge win over a miss.
- SDK — both are one call in every AWS SDK (
GetItem/PutItemvsGetObject/PutObject).
The core split — one sentence
Small, hot, expiring entries → DynamoDB. Big, cold, long-lived blobs → S3.
Cache design tips (either store)
- Hash the inputs into the key. Build the key from everything that changes the output — parameters, model or engine version, and a format/prompt version you bump to invalidate old entries.
- Don't store more than you need. A hash key means the raw input (which may be private) never has to be stored.
- Make the cache best-effort. If the store is unreachable, treat it as a miss — never fail the request because the cache failed.
- Give users a way round it. A "refresh" / bypass flag for when a cached answer is stale or wrong.
- Different TTLs for different inputs. Output from immutable input can live for months; output derived from a live web page should expire in hours or days.
- DynamoDB: check TTL on read. Because deletion is lazy, compare the item's
ttlto now and treat an expired item as a miss. - S3: use a dedicated prefix (for example
cache/) so one lifecycle rule can expire the whole cache.
Other options
- ElastiCache / Valkey (Redis-compatible) — sub-ms, in-memory, per-key TTL. Best for very hot, high-QPS caches; you pay for nodes (or serverless capacity) whether it's busy or not.
- DAX — an in-memory cache in front of DynamoDB, microsecond reads, no code change beyond the client.
- S3 Express One Zone — single-digit-ms S3 in one AZ, higher storage price; worth a look if you want S3 semantics at DynamoDB-like latency.
- CDN / HTTP cache — if the result is a public GET response,
Cache-Controlheaders may be all the cache you need.
Links
| DynamoDB | S3 | |
|---|---|---|
| Pricing | DynamoDB pricing | S3 pricing |
| Expiry | Time to Live (TTL) | Lifecycle expiration |
| Limits | Service quotas | Performance guidelines |
| Conditional writes | Condition expressions | Conditional requests |
| Large items | Storing large items (the hybrid pattern) | — |
Caveats
- Prices move. Figures are us-east-1 list prices (DynamoDB on-demand after the Nov 2024 cut, S3 Standard). Your region, free tier and data-transfer charges change the totals — recheck the pricing pages.
- Latency figures are typical, not guaranteed. Measure from your own compute region; cross-region calls add far more than the store itself.
- TTL is not a timer. Neither DynamoDB TTL nor S3 lifecycle deletes at the exact moment; both are background sweeps. Build expiry checks into the read if staleness matters.
- The scorecard assumes a cache — exact-key lookup of generated results. For other workloads (data lake, media library, transactional records) the answer can flip.
Disclaimer: These documents are personal notes. The information may be incomplete, out of date or wrong, so check it before you rely on it.