Choose DynamoDB when you can list your access patterns up front and your traffic is spiky: it speaks HTTP, so it scales with Lambda concurrency by design. Choose RDS (PostgreSQL or Aurora) when you need ad-hoc queries, joins and multi-row invariants, and accept that Lambda needs RDS Proxy in front of it. Many production platforms end up running both, and that is a legitimate architecture, not a failure. This guide walks through the criteria that actually decide it.
Do you know your access patterns up front?
This is the first question because DynamoDB makes it non-negotiable: you design the table around the queries you will run, before writing code. If you can enumerate them (fetch order by id, list a customer's orders newest first), DynamoDB will serve them in single-digit milliseconds. If you cannot, a relational schema is the safer default.
AWS's own NoSQL design guidance is explicit on this point: model the table after the application's access patterns, not after the entities. SQL takes the opposite bet. A normalized schema answers questions nobody anticipated: the marketing filter, the finance export, the join a product manager asks for on a Friday. Reporting and back-office tools speak SQL natively; almost none speak DynamoDB.
How does Lambda's connection model change the choice?
Radically, and this is the criterion teams discover in production. DynamoDB is an HTTP API: every concurrent Lambda execution environment makes stateless requests, so 5 or 5,000 concurrent invocations look the same to it. PostgreSQL holds stateful TCP connections, and every Lambda environment opens its own, which is exactly what kills it under load.
A traffic spike creates hundreds of environments, each opening a connection; Postgres refuses new ones once max_connections is hit, and the backend fails while the database sits mostly idle. RDS Proxy fixes this by owning a warm pool and multiplexing Lambda requests onto far fewer real connections. It is not optional at scale; treat it as part of the RDS bill. The same reasoning applies to any pooled resource, as we detail in Lambda vs servers.
The query models differ as much as the connection models:
// DynamoDB: a customer's latest orders (SDK v3 DocumentClient)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const { Items } = await doc.send(new QueryCommand({
TableName: 'orders',
KeyConditionExpression: 'customerId = :c',
ExpressionAttributeValues: { ':c': 'cus_123' },
ScanIndexForward: false, // newest first
Limit: 20,
}));
// PostgreSQL via RDS Proxy: same intent, plus a join DynamoDB cannot do
import { Pool } from 'pg';
// One pool per execution environment, created outside the handler
const pool = new Pool({ host: process.env.PROXY_ENDPOINT, max: 1 });
const { rows } = await pool.query(
`SELECT o.id, o.total, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.customer_id = $1
ORDER BY o.created_at DESC
LIMIT 20`,
['cus_123'],
);
How do scaling and operations compare in 2026?
Both sides are credible in 2026, but they scale along different axes. DynamoDB on-demand mode absorbs traffic from zero to extreme peaks with no capacity planning, and its operational surface is close to zero. Aurora Serverless v2 scales compute in fine-grained increments, quickly, but it scales a database instance, not individual requests.
The ops load follows the same split. With DynamoDB you tune keys and watch throttling metrics; there is no engine to patch, no vacuum to schedule, no failover drill. With Aurora you still own engine versions, parameter groups, connection limits and index health, even in serverless form. Neither burden is huge; they are simply different jobs.
Unsure which database your access patterns actually point to? Describe your system: a one-page diagnosis within 48 hours.
Get my diagnosis →Who wins on transactions and multi-row invariants?
Relational databases, and it is not close when invariants span many rows. DynamoDB does have ACID transactions: TransactWriteItems groups up to 100 items across tables, all or nothing. But there are no interactive transactions, no foreign keys, and no constraints the engine enforces for you; your application code carries the invariants.
If the core of your domain is money movement, inventory that must never go negative, or anything an auditor will read, PostgreSQL's constraints, serializable isolation and mature tooling deserve real weight. If your writes are mostly independent events keyed by one entity, DynamoDB's conditional writes cover most needs.
What shape is your cost curve?
Ignore absolute prices and look at the shape. DynamoDB on-demand bills per request: cost tracks usage exactly, so spiky traffic with a low average is cheap, and idle costs almost nothing beyond storage. RDS bills for provisioned capacity by the hour: a steady, highly utilized instance is efficient, an idle one is pure waste.
The crossover works both ways. A table hammered constantly at high, predictable volume can cost more than a right-sized instance doing the same work (provisioned capacity narrows the gap). Aurora Serverless v2 softens the idle problem but keeps the instance-shaped curve. Model your real traffic histogram before trusting either intuition.
How expensive is changing your mind later?
Expensive in both directions, which is why this decision deserves more than a default. A single-table DynamoDB design encodes your access patterns into key structure; new patterns mean new GSIs at best, backfilled table redesigns at worst. Leaving SQL means rewriting every join and constraint your code silently relies on.
Schema migrations on a relational database are routine and well-tooled; migrating a live table between database families is a project measured in months. Our rule: pick DynamoDB only when the access patterns feel stable, and keep a repository layer between handlers and the database so a future move stays survivable.
Is the honest answer both?
Often, yes. The pattern many platforms converge on: DynamoDB for hot-path event and state data (sessions, orders in flight, device state), PostgreSQL for relational reporting and back-office, with events syncing one to the other. Each database does the job it was built for, and neither is forced into the other's role.
DynamoDB Streams or domain events through EventBridge feed writes into Postgres asynchronously; reporting tolerates seconds of lag, and the hot path never waits on a join. We described the plumbing in our EventBridge, SQS and Lambda pattern guide. The price is eventual consistency between stores and one more pipeline to monitor: pay it only when both workloads genuinely exist.
DynamoDB vs RDS at a glance
The table below compresses the criteria. Read it row by row against your workload, not as a scorecard: one decisive row (say, unknowable access patterns) outweighs three comfortable ones. In our experience, the connection model and the cost shape are the rows teams most often get wrong.
| Criterion | DynamoDB | RDS (+Proxy) / Aurora |
|---|---|---|
| Access patterns | Known up front, designed into keys | Ad-hoc queries, joins, evolving needs |
| Lambda connection model | Stateless HTTP, scales with concurrency | Pooled TCP via RDS Proxy, required at scale |
| Scaling | Per request, on-demand to extreme peaks | Instance-based; Aurora Serverless v2 scales compute fast |
| Transactions | TransactWriteItems, up to 100 items, no interactive transactions | Full ACID, constraints, serializable isolation |
| Reporting | Weak: export or sync elsewhere | Native SQL, every BI tool speaks it |
| Cost shape | Per request: loves spiky, low-average traffic | Per instance-hour: loves steady, high utilization |
| Ops load | Near zero: keys and throttling metrics | Engine versions, parameters, indexes, failover drills |
The decision checklist
Before committing either way, put these on paper:
- List every access pattern; if the list will not stay stable, lean SQL.
- Count how the back office and finance will query the data; that work lands in SQL either way.
- Sketch your traffic histogram: spiky and low-average favors DynamoDB, steady and high favors instances.
- If Lambda talks to RDS, budget RDS Proxy from day one.
- Write down the invariants that span rows; if they are core, favor relational.
- Keep a repository layer so the choice stays reversible at the code level.
- Consider the hybrid before forcing one database into the other's job.