Choosing S3, RDS, or DynamoDB wrong costs you in performance, cost, and scalability. Here is a practical decision guide based on your actual access patterns.
A team at a Bengaluru e-commerce startup stored their product catalogue in RDS. 2 million products, each with different attributes — t-shirts have size and colour, phones have RAM and storage, books have ISBN and author. Every attribute was a column. Their schema had 47 columns and half of them were NULL for any given product. Queries were slow. Adding a new product type required a schema migration. They migrated to DynamoDB and query times dropped from 180ms to 8ms.
The same team stored user profile images in DynamoDB. Images are binary objects. DynamoDB items have a 400 KB limit per item. They stored base64-encoded thumbnails in DynamoDB items. It worked until they added high-resolution images. They migrated to S3 and API response times dropped from 1.2 seconds to 90ms because S3 returns direct presigned URLs rather than binary data through the API.
The right storage choice is never about which service is "better." It is always about which service matches your data model and access pattern.
Each service is built around a fundamentally different model:
S3 — Object Storage:Any file, any size, stored as a named object in a bucketAccess via HTTP API: GET /bucket/key → download the fileNo schema. No relationships. No queries on file contents.Best for: files, media, backups, logs, large datasets RDS — Relational Database:Tables with fixed columns, rows with valuesAccess via SQL: SELECT, JOIN, GROUP BY, WHERESchema enforced. ACID transactions. Complex queries.Best for: structured data with relationships and complex queries DynamoDB — Key-Value / Document:Tables with items, flexible attributes per itemAccess via key: get item by partition key + optional sort keyNo schema enforcement. Millisecond latency at any scale.Best for: simple key-based access at massive or unpredictable scaleThe mistake is trying to use one service for all three purposes.
S3 is for files and objects — anything you store as a complete unit and retrieve as a complete unit.
At Hotstar, 50 petabytes of video content lives in S3. Videos are retrieved as complete files for streaming, not queried by attribute. S3 stores them durably across multiple AZs, delivers them through CloudFront to edge locations worldwide, and costs $0.023/GB/month in Standard tier. No database could store or serve video content at this cost or scale.
S3 is correct for:
S3 is wrong when:
S3 cost reality:
S3 Standard: $0.023/GB/monthS3 Standard-IA: $0.0125/GB/month (+ $0.01/GB retrieval)S3 Glacier Flexible: $0.004/GB/month (+ $0.01/GB retrieval, hours wait) 1 TB of logs:All in Standard: $23/monthLifecycle to IA after 30 days: ~$12/monthLifecycle to Glacier after 90 days: ~$4/month Set lifecycle rules from day one. Never pay Standard prices for data nobody reads.RDS is for structured relational data where your queries cannot be predicted at schema design time.
Zerodha's trading ledger has debits, credits, balances, accounts, instruments, and orders. Each trade creates entries across multiple tables atomically. A credit to one account is simultaneously a debit to another. This is a two-table transaction that must either fully succeed or fully fail. DynamoDB's transaction support exists but is limited — you pay per transaction and the programming model is more complex. PostgreSQL on RDS handles this with native ACID transactions, familiar SQL, and the ability to run arbitrary queries against the data that the engineering team has not thought of yet.
RDS is correct for:
RDS is wrong when:
RDS vs Aurora:
Standard RDS MySQL: baselineAurora MySQL: 5x faster, 6 copies across 3 AZs, auto-grows to 128 TBUse Aurora for any new production workload unless budget is a constraint.RDS connection management:
RDS has a maximum connection limit based on instance size. At scale, Lambda functions and containerised services can exhaust this limit. Use RDS Proxy — it pools connections and multiplexes thousands of Lambda connections through a smaller pool of real database connections.
DynamoDB is for simple, fast, key-based access at any scale — from one request per day to millions per second.
At PhonePe, every UPI transaction creates a state record — a temporary object with status (initiated, processing, completed, failed) that must be checked in under 10 milliseconds. Millions of transactions per day. Each state record is accessed by transaction ID only — no joins, no aggregations. DynamoDB returns any item by primary key in under 5ms consistently, regardless of whether the table has 1,000 items or 10 billion items. No relational database can match this at scale without significant architecture complexity.
DynamoDB is correct for:
DynamoDB is wrong when:
DynamoDB cost:
On-Demand mode:Read: $0.00013 per Read Request Unit (1 RRU = 4 KB read)Write: $0.00065 per Write Request Unit (1 WRU = 1 KB write) 1 million 1KB writes per day:DynamoDB On-Demand: 1M × $0.00065 = $0.65/day = ~$20/month Provisioned mode: 30-40% cheaper for predictable traffic| Dimension | S3 | RDS | DynamoDB |
|---|---|---|---|
| Data type | Files and objects | Structured rows and columns | Items with flexible attributes |
| Query language | None (key-based GET) | SQL — full JOIN, GROUP BY | Key-based — partition key + sort key |
| Max item/row size | 5 TB | Limited by disk | 400 KB per item |
| Latency | 50-200ms (first byte) | 1-50ms (indexed query) | Single-digit ms (key lookup) |
| Scaling model | Unlimited, automatic | Vertical + Read Replicas | Automatic, unlimited |
| ACID transactions | No | Yes | Yes (limited, extra cost) |
| Schema | None | Fixed — migrations required | Flexible per item |
The most effective architectures use all three together, each handling what it does best:
Swiggy order platform: S3: Restaurant menu photos and videos Invoice PDFs for completed orders Application logs for analytics RDS (Aurora MySQL): Orders table (order_id, user_id, restaurant_id, total, status) Restaurants table (restaurant_id, name, location, rating) Complex queries: GROUP BY restaurant for analytics, JOIN for order history DynamoDB: Active session tokens (expire via TTL after 24 hours) Real-time delivery driver locations (updated every 10 seconds per driver) Rate limiting counters (user X made N API calls in the last minute)None of these are interchangeable. Storing restaurant photos in RDS or DynamoDB would be architecturally wrong. Storing driver location in RDS would be too slow. Storing order history in DynamoDB would make reporting queries painful.
| Requirement | Use S3 | Use RDS | Use DynamoDB |
|---|---|---|---|
| Store images, videos, files | Yes | No | No |
| SQL joins and GROUP BY | No | Yes | No |
| Get item by ID in <10ms | No | Maybe | Yes |
| ACID cross-table transactions | No | Yes | Partial |
| Store 100 TB+ at low cost | Yes | No | Yes |
| Unknown query patterns | No | Yes | No |
Follow these rules before finalising your storage choice:
REMEMBER THIS**References and Further Reading** * [AWS database selection guide](https://aws.amazon.com/products/databases/) — AWS's own comparison of all database services * [DynamoDB best practices](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html) — single-table design and access pattern design * [Amazon Aurora vs RDS](https://aws.amazon.com/rds/aurora/faqs/) — when Aurora is worth the extra cost
DynamoDB supports single-table design where data that would normally require joins is denormalised into one table using composite keys. This works well for known, fixed access patterns but becomes complex for ad-hoc querying. For data with genuinely relational structure and unpredictable query patterns, RDS or Aurora is the more maintainable choice.
The maximum S3 object size is 5 TB. Single PUT requests are limited to 5 GB, so any file above 5 GB must use multipart upload. AWS recommends multipart upload for files above 100 MB for better performance — it uploads parts in parallel and allows retry of individual failed parts rather than the entire file.
Discussion0