Yes, DynamoDB is serverless: there is no instance to size, no engine version to upgrade, no patch window, no failover to configure, and a table sitting at zero traffic asks nothing of you to stay alive. But serverless describes the operational model, not automatically the billing model. DynamoDB has two capacity modes and only one of them bills per request. Pick the other one without thinking and you pay for reserved throughput every hour of the month, on a database everyone in the room still calls serverless.
What does AWS actually run for you?
The storage layer, in full. Every item is written to storage replicated across three Availability Zones in the Region, and the replication, the failover between copies, the splitting of partitions as a table grows and the patching of everything underneath happen without a maintenance window, a version choice or an approval from you. There is no read replica to promote and no minor upgrade to schedule at midnight.
The access path is just as hands-off. A table has no endpoint to place inside a VPC and no connection limit to exhaust: you call an HTTPS API signed with IAM credentials, and the service decides which machine answers. The how it works documentation describes the partitioning underneath, which is worth reading precisely because you cannot touch it. What stays yours is everything above the API.
| Concern | AWS manages | You still own |
|---|---|---|
| Servers and patching | All of it: no instances, no OS, no engine version | Nothing |
| Availability | Replication across three Availability Zones, automatic failover | Single Region or global tables, and backup policy |
| Storage scaling | Partitions split and grow with the table, no ceiling to raise | Item size, attribute bloat, what you keep and for how long |
| Throughput | Absorbs traffic within table and partition limits | The capacity mode, and the numbers if you choose provisioned |
| Data model | Nothing | Partition key, sort key, indexes, access patterns |
| Cost | Nothing | All of it: mode, index count, item size, read consistency |
Is DynamoDB pay-per-use?
In on-demand mode, yes. You are billed for the read and write request units your calls actually consume, plus storage, so a table nobody touches costs storage alone and a quiet weekend shows up as a quiet line on the bill. That is the billing shape people picture when they say serverless, and it is why on-demand is the sane default for a new table.
In provisioned mode, no. You declare read and write capacity units per second and pay for them by the hour whether the requests arrive or not. Auto scaling moves those numbers within a range you set, but it reacts to published metrics rather than to the request in flight, so a sharp spike can throttle while capacity catches up. Reserved capacity lowers the rate in exchange for a term commitment, which is the least serverless thing in the service. The capacity mode documentation covers the switching rules, and the pricing page carries the current rates.
| Dimension | On-demand | Provisioned |
|---|---|---|
| Billing unit | Per request unit consumed, plus storage | Per capacity unit reserved per hour, plus storage |
| Idle cost | Storage only | The full reserved capacity, all month |
| Spike behavior | Absorbed, within table and account limits | Throttled until auto scaling catches up |
| Planning work | None | Sizing, alarms, auto scaling ranges, a review cadence |
| Commitment | None | Reserved capacity for a term, if you want the lower rate |
| When it wins | New tables, spiky traffic, low average, unknown load | Steady, measured, high utilization you can predict |
What do you still own?
The data model, and it decides more than the mode does. The partition key sets how traffic spreads: a key with low cardinality, or one big tenant among small ones, concentrates requests on a single partition and produces throttling that looks like a service fault and is a modeling fault. AWS publishes partition key design guidance because this is the failure it sees most.
Three more lines stay on your side of the fence. Every global secondary index is a second copy of the projected attributes, with its own storage and its own throughput, so index count is a cost decision disguised as a query decision. Strongly consistent reads consume more capacity than eventually consistent ones, which makes read consistency a budget choice. And item size drives read cost directly, so the habit of stuffing a blob into an item is paid for on every single read of that item.
Not sure whether your tables are serverless in operations only, or on the bill too? Describe your system: a one-page diagnosis within 48 hours.
Get my diagnosis →Do you need single-table design?
Not as an entry ticket. Single-table design is an optimization with a specific payoff: overloading one key space lets a single query return an order and its lines together, instead of the round trips a relational join would have spared you. The NoSQL design guidance starts from that idea, and it has its reasons.
The price is legibility. The table stops describing itself: keys become opaque, the console stops being readable, and the schema now lives in application code where a newcomer has to reconstruct it. On-demand tables bill only what they store and serve, so splitting entities across several tables adds no fixed line to the bill. For a service with a handful of entities and no cross-entity query on the hot path, separate tables are easier to evolve and cost nothing extra. Decide on access patterns, not on orthodoxy.
How does DynamoDB pair with Lambda?
Without a connection pool, which is the whole point. DynamoDB is an HTTPS API authenticated with IAM, so a thousand concurrent Lambda execution environments are simply a thousand independent callers. There is no maximum connection count to hit, no proxy to run and no VPC placement to get wrong. That is the structural difference with PostgreSQL on RDS, where Lambda concurrency turns into connection pressure and RDS Proxy stops being optional. We walk through that comparison in our DynamoDB vs RDS guide for serverless Node.js backends.
// Created once per execution environment, reused across invocations.
// No pool to size, no connection to open, no proxy in front.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
const ddb = DynamoDBDocumentClient.from(
new DynamoDBClient({ maxAttempts: 3 }),
);
export const handler = async (event: { orderId: string }) => {
const { Item } = await ddb.send(
new GetCommand({
TableName: process.env.TABLE_NAME,
Key: { pk: "ORDER#" + event.orderId, sk: "META" },
}),
);
return Item ?? null;
};
Two habits matter here. Build the client outside the handler, so the SDK reuses the underlying HTTPS connection across invocations in the same environment instead of paying the handshake every time. And set retries and timeouts explicitly, because throttling is a normal condition in this service rather than an emergency, and the default behavior deserves to be a decision rather than an accident.
When does DynamoDB stop being the right serverless database?
When you cannot name the queries in advance. DynamoDB answers the access patterns you designed keys for, quickly and at any scale; it has no join, no free-form filter across the whole table that does not degrade into a scan, and no aggregate query. A product where analysts ask a new question every week is not a modeling challenge, it is the wrong engine.
The usual answers are boring and correct. Keep a relational engine for the query set you cannot predict, weighing the cost shapes as we do in our Aurora Serverless v2, RDS and DynamoDB cost comparison. For reporting, use the native export to Amazon S3 and query the copy with Athena, so analytics never lands on the hot path. For full text and faceted search, stream changes into a search engine. Running DynamoDB on the write path and something else on the read path is a normal architecture, not an admission of defeat.
The decision rule
DynamoDB is serverless in operations for everybody, and serverless in billing only in on-demand mode. Everything else is a choice you make and pay for.
- Access patterns you can list today: DynamoDB fits. A query set that keeps changing: use a relational engine.
- Start every new table in on-demand mode, the only mode that bills the way the word suggests.
- Move to provisioned only when a month of real metrics shows steady, predictable utilization, and price the commitment before making it.
- Check the partition key for cardinality and for one tenant dwarfing the others, before launch rather than during the incident.
- Count your global secondary indexes: each one is another copy of the data, with its own storage and throughput.
- Send reporting and analytics to an S3 export, never to the production table.
- From Lambda: client outside the handler, explicit retries, no pool, no VPC, IAM for auth.
If the choice between capacity modes is really a question about your traffic curve, that is the work: our AWS decision guides start from the curve, not from the service name.