AWS Databases: RDS and DynamoDB
Managed databases on AWS: what RDS handles for you, Multi-AZ vs read replicas, backups, and DynamoDB's key-value model, partition keys, and capacity modes.
You can run PostgreSQL on an EC2 instance yourself. Install the package, set up backups with cron, configure a standby, patch it on Tuesday nights, and page yourself when the disk fills. People do this, and it works right up until the night the primary dies and you discover the failover you never tested does not actually fail over. Managed databases exist so you stop doing that. AWS runs the database process for you and takes the operational load off your hands - patching, backups, replication, failover - so you spend your time on the schema and the queries, not the babysitting. This guide covers the two you will reach for most: RDS for relational workloads and DynamoDB for key-value at scale, plus how to decide which one a given workload wants.
What "managed" actually buys you
When you launch an RDS instance or a DynamoDB table, you are not just renting a machine with a database pre-installed. You are handing AWS a specific list of chores that are boring, easy to get subtly wrong, and catastrophic when they fail:
- Patching - minor version upgrades and OS security patches happen in a maintenance window you pick, not by you SSHing in at midnight.
- Backups - automated daily snapshots plus continuous transaction logs, so you can restore to any second within your retention window. You did not write the backup script and you did not forget to test the restore.
- Failover - if the underlying host dies, a managed database with high availability promotes a standby automatically, usually in a minute or two, without you awake to run the runbook.
- Replication - keeping copies in sync is handled by AWS, not by you tuning replication settings and watching for lag.
The trade is control. You do not get root on the host, you cannot install arbitrary extensions that AWS has not blessed, and you configure the engine through parameter groups rather than editing config files directly. For the overwhelming majority of workloads that is a good trade: the things you give up are things you did not want to own, and the things you gain are the things that used to wake you up. The rule of thumb: do not run a database by hand on an EC2 box unless you have a specific, defensible reason. "It felt cheaper" is not one - the labor and the risk cost more than the managed premium.
RDS: managed relational databases
RDS (Relational Database Service) runs standard relational engines for you: PostgreSQL, MySQL, MariaDB, Oracle, and SQL Server. It is still the same Postgres or MySQL your application talks to - same wire protocol, same SQL, same drivers - AWS just operates the instance underneath. You point your app at an endpoint (a DNS name like mydb.abc123.us-east-1.rds.amazonaws.com) and use it exactly as you would any database.
The core knobs are the instance class (CPU and memory, like db.r6g.large), the storage (size and type, typically gp3 SSD with a provisioned IOPS option for heavy workloads), and the engine version. You size it, AWS runs it.
aws rds create-db-instance \
--db-instance-identifier app-prod \
--engine postgres \
--engine-version 16.3 \
--db-instance-class db.r6g.large \
--allocated-storage 100 \
--storage-type gp3 \
--master-username appadmin \
--manage-master-user-password \
--multi-az \
--db-subnet-group-name app-private-subnets \
--vpc-security-group-ids sg-0abc123
Most of the interesting decisions below hang off that one command: whether to set --multi-az, which subnet group the database lands in, and how backups and parameters are configured.
Multi-AZ vs read replicas: two different jobs
This is the distinction people get wrong most often, because both involve "another copy of the database" and the names blur together. They solve opposite problems.
Multi-AZ is for availability. When you enable it, RDS maintains a synchronous standby copy in a different Availability Zone (a separate datacenter in the same region). You never talk to the standby - it is not there for capacity. It exists so that if the primary's host, storage, or entire AZ fails, RDS automatically promotes the standby and repoints the endpoint DNS to it, typically within a minute or two. Your application reconnects to the same endpoint and keeps going. This is your insurance against hardware and datacenter failure, and it is the single most important box to tick for anything production.
Read replicas are for read scaling. A read replica is an asynchronous copy you can send read queries to, spreading read load across multiple instances. If your app is read-heavy (reporting, dashboards, product pages that read far more than they write), you route reads to replicas and keep writes going to the primary. Replicas have their own endpoints - the app has to be written to send reads there deliberately. Because replication is asynchronous, a replica can lag slightly behind the primary, so a read right after a write might not see it. That is fine for a dashboard and wrong for "confirm the order I just placed."
The one-line version: Multi-AZ keeps you up (automatic failover, you never query the standby); read replicas make you faster at reading (you query them on purpose, and they can lag). They are not alternatives - a serious setup often has both: Multi-AZ for the primary's availability and read replicas for scale.
Backups: automated backups and snapshots
RDS gives you two backup mechanisms and it is worth knowing which is which.
Automated backups are the daily ones AWS takes for you, plus continuous archiving of the transaction logs. Together they enable point-in-time recovery (PITR) - you can restore the database to any second within your retention window (1 to 35 days). This is what saves you when someone runs DELETE without a WHERE clause at 3pm: you restore a fresh instance to 2:59pm. Automated backups are deleted when you delete the instance, so they are not for long-term keeping.
Manual snapshots are point-in-time copies you trigger and that live until you explicitly delete them. Use them before a risky migration, for compliance retention, or to copy a database to another region or account. A restore from either always creates a new instance - you never restore in place over the live one.
# take a snapshot before a scary migration
aws rds create-db-snapshot \
--db-instance-identifier app-prod \
--db-snapshot-identifier app-prod-pre-migration
# restore to a point in time into a brand-new instance
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier app-prod \
--target-db-instance-identifier app-prod-restored \
--restore-time 2026-07-07T14:59:00Z
The failure mode to avoid is assuming backups are running and never testing a restore. A backup you have never restored is a hope, not a backup. Do a restore drill so the runbook is real before you need it.
Parameter groups and subnet groups
Two supporting objects control how the database is configured and where it lives.
A parameter group is how you tune the engine, since you cannot edit postgresql.conf directly. It is a named set of engine settings (max_connections, work_mem, shared_buffers and so on) that you attach to an instance. Change a value in the group and it applies to every instance using it; some parameters apply live, others need a reboot. Keeping settings in a named group rather than per-instance is what makes them reproducible.
A subnet group is a list of subnets, across at least two AZs, that RDS is allowed to place the database in. It matters for two reasons: Multi-AZ needs subnets in more than one AZ to have somewhere to put the standby, and the subnet group is what determines whether your database is reachable from the internet or not.
Put the database in a private subnet
A database should almost never be publicly reachable. The pattern is to run it in private subnets - subnets with no route to an internet gateway - so nothing from the public internet can even attempt a connection. Only the application tier, running in the same VPC, can reach it.
You enforce this with two layers. First, the subnet group points RDS at private subnets, and you launch with public accessibility turned off, so the instance gets no public IP. Second, the database's security group allows inbound traffic only on the database port (5432 for Postgres, 3306 for MySQL) and only from the application tier's security group - not from an IP range, from the app's security group itself. That way "who can talk to the database" is defined by role ("the app servers"), not by brittle IP lists, and it keeps working as instances come and go.
Internet
|
[ public subnet ] -> load balancer, NAT
|
[ app tier ] -> EC2 / ECS, in the VPC, security group sg-app
| (port 5432, allowed only from sg-app)
[ private subnet ] -> RDS, no public IP, security group sg-db
If this VPC and subnet layout is new to you, the AWS VPC networking guide walks through public vs private subnets, route tables, and security groups in full. The database tier is the canonical example of "reachable only from inside" - get it wrong and you have put your production data on the open internet.
Aurora: the cloud-native option
Aurora is AWS's own database engine, wire-compatible with PostgreSQL and MySQL - your app cannot tell the difference, but the storage layer underneath is rebuilt for the cloud. It separates compute from storage: the storage is a distributed layer that keeps six copies across three AZs automatically, replicas are faster to spin up and lag less because they read the same shared storage, and failover is quicker than standard RDS. Aurora Serverless can even scale capacity up and down (and to zero, on v2) with load.
You pay a premium over plain RDS for it. Reach for Aurora when you want RDS's managed relational model but need better availability, faster read scaling, or more elastic capacity than the standard engines give you. For a straightforward Postgres workload, plain RDS is perfectly good and cheaper; Aurora is the upgrade when scale or availability demands it.
DynamoDB: managed NoSQL at scale
DynamoDB is a different kind of database and a different way of thinking. It is a fully managed key-value and document store - NoSQL, not relational. There are no joins, no SELECT * WHERE across arbitrary columns, no schema you migrate. You store items (like JSON objects) in tables, and you look them up primarily by their key. In exchange for giving up the flexibility of SQL, you get two things that are very hard to get from a relational database: single-digit-millisecond latency at effectively any scale, and near-zero operational work. There are no instances to size, no failover to configure, no version to patch. You make a table and you use it.
When DynamoDB fits
DynamoDB is a specialist, and it is excellent when the workload matches its shape:
- You know your access patterns in advance. DynamoDB rewards you when you can say "I always look this up by user ID" or "I fetch a session by session ID." It punishes you when you need to slice the data ten different ways you did not anticipate - that is what SQL is for.
- You need massive scale. DynamoDB holds its latency flat whether the table has a thousand items or a hundred billion. Shopping carts, session stores, user profiles, IoT event ingestion, leaderboards - high-volume, key-based lookups are its sweet spot.
- You want serverless operations. No instance, no capacity planning of servers, pay for what you use. It fits naturally with Lambda and event-driven architectures where you do not want a database server to manage at all.
If you find yourself wanting to run reporting queries, ad-hoc joins, or "give me everything where status is X and created after Y and sorted by Z," you are describing a relational workload and you should use RDS. Bending DynamoDB into that shape is possible and painful.
Partition keys and the hot-partition trap
The single most important thing to understand about DynamoDB is the partition key, because it decides both how you look data up and how well the table performs.
Every item has a primary key. In the simplest form that is just a partition key (say userId). You can also use a composite key: a partition key plus a sort key (say userId as partition and timestamp as sort), which lets you store many items under one partition key and query a range of them - "all events for this user, newest first."
Under the hood, DynamoDB hashes the partition key to decide which physical partition stores the item, and it spreads your table's capacity evenly across partitions. This is the source of its scale - and the source of its worst trap. If your partition key funnels most traffic to one value, all that traffic lands on one partition, which has only a slice of the table's total capacity. That is a hot partition: the table throttles requests even though the table as a whole is nowhere near its limit. The classic mistake is a partition key like status (where most items are "active") or a single "current day" key that every write hammers.
The fix is to choose a partition key with high cardinality and even access - userId, orderId, a device ID - something with many distinct values so traffic spreads out. Design the key around how you actually read and write, not around how the data is categorized. Getting the partition key right is the DynamoDB equivalent of getting your indexes right in SQL: it is the difference between a table that scales forever and one that mysteriously throttles at low load.
# a table with a composite key: partition (userId) + sort (createdAt)
aws dynamodb create-table \
--table-name user-events \
--attribute-definitions \
AttributeName=userId,AttributeType=S \
AttributeName=createdAt,AttributeType=S \
--key-schema \
AttributeName=userId,KeyType=HASH \
AttributeName=createdAt,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST
# fetch all events for one user, most recent first
aws dynamodb query \
--table-name user-events \
--key-condition-expression "userId = :u" \
--expression-attribute-values '{":u": {"S": "user-42"}}' \
--scan-index-forward false
Note query (efficient, uses the key) versus scan (reads the whole table, avoid it in hot paths). If you find yourself scanning, your access pattern and your key design are fighting each other.
On-demand vs provisioned capacity
DynamoDB has two billing modes and the choice comes down to how predictable your traffic is.
- On-demand (
PAY_PER_REQUEST) - you pay per read and write request and DynamoDB scales instantly to whatever traffic arrives. No capacity to plan, no throttling from under-provisioning. It is the right default: perfect for spiky, unpredictable, or new workloads where you do not yet know the traffic shape. - Provisioned - you specify the read and write capacity units per second you expect (optionally with auto-scaling). It is cheaper per request if your traffic is steady and predictable enough to size well, but you get throttled if you under-provision and you waste money if you over-provision.
Start with on-demand. It removes a whole category of "we got throttled because traffic spiked" incidents and you do not have to guess. Move to provisioned only once you have a stable, well-understood traffic pattern and the per-request savings are worth the capacity management. The whole point of DynamoDB is not managing servers; on-demand keeps that promise for capacity too.
The latency promise
The reason people reach for DynamoDB despite the constraints is performance. A key-based lookup returns in single-digit milliseconds consistently, whether the table has a thousand rows or a hundred billion, whether you send it ten requests a second or ten million. Relational databases can be fast, but keeping them fast as data and traffic grow takes tuning, bigger instances, read replicas, and caching. DynamoDB gives you flat latency at scale as a property of the system, provided you designed your keys to spread the load. That guarantee - constant performance regardless of size - is the thing you cannot easily buy from RDS, and it is why high-scale key-value workloads live here.
How to choose: relational or NoSQL
The choice is not about which database is better - it is about the shape of your data and how you query it.
Reach for RDS (relational) when:
- Your data has relationships you query across - joins, foreign keys, "orders belonging to customers belonging to accounts."
- You need flexible, ad-hoc queries you cannot fully predict up front - filtering and sorting by many different columns, reporting, analytics.
- You want strong transactional guarantees across multiple rows and tables.
- Your team already knows SQL and the workload is a normal application backend. This is most applications, and RDS is the safe default.
Reach for DynamoDB (NoSQL) when:
- Your access patterns are known and simple - you look items up by a key you already have.
- You need massive scale with flat, single-digit-millisecond latency.
- You want zero server operations and a serverless fit (Lambda, event-driven).
- You do not need joins or rich ad-hoc queries against the data.
The honest default for a new application backend is RDS (or Aurora): SQL is flexible, your team knows it, and it will not paint you into a corner when a new query shows up next quarter. Choose DynamoDB deliberately, for the specific workloads that match its shape - the high-scale, key-based, predictable-access parts of your system - rather than as a general-purpose default. Plenty of real systems use both: RDS for the core transactional data and DynamoDB for the session store, the event firehose, or the leaderboard. Match the tool to the access pattern and you get the best of each; force one tool onto the wrong pattern and you fight it forever.
The shape of it
Managed databases exist so you stop running a database by hand on an EC2 box - AWS takes patching, backups, replication, and failover off your plate. RDS gives you standard relational engines with the operational load handled: use Multi-AZ to stay up through failures (automatic failover, you never query the standby) and read replicas to scale reads (you query them on purpose, and they can lag) - they solve different problems. Automated backups give you point-in-time recovery; snapshots are the copies you keep. Put the database in a private subnet reachable only from the app tier's security group, and reach for Aurora when you need more availability or elasticity than plain RDS. DynamoDB is the specialist: managed key-value NoSQL with single-digit-millisecond latency at any scale, ideal when your access patterns are predictable and your scale is large - design the partition key for high cardinality to dodge hot partitions, and start with on-demand capacity. Relational for flexible queries and relationships, NoSQL for known-pattern lookups at scale. Pick per workload, not per religion.