Serverless promises that you only pay for what you use. That’s true — and it’s exactly why serverless bills surprise people. Usage is the variable you stopped watching the day you stopped provisioning servers.
The bills that hurt rarely come from one expensive service. They come from multipliers nobody wrote down: a retry policy that triples invocations during an outage, a fan-out that turns one upload into forty events, a Lambda in a VPC quietly pushing every byte through a NAT gateway, a debug log line that ships a full payload on every request. Each is reasonable on its own. Together they’re the invoice.
This is the reference architecture I keep coming back to for request/response APIs on AWS, and the habits that keep its bill boring as traffic grows.
Price a request before you build it
I treat the cost of a request like its latency budget: a number I estimate before writing code and instrument once it ships. The estimate doesn’t need to be precise. It needs to exist, because it forces you to write down every hop.
For a typical API call, the itemised bill looks like this:
- The front door — API Gateway charges per request. For most APIs an HTTP API does the job at a fraction of the per-request price of a REST API; reach for REST APIs only when you need their extras (usage plans, request validation, WAF directly on the stage, private endpoints).
- Compute — Lambda charges per invocation plus GB-seconds: memory × duration. Every millisecond a function spends waiting on something else is billed.
- Data — DynamoDB on-demand charges per read and write request unit. Reads are metered in 4 KB units and writes in 1 KB units, so item size is a cost decision, not just a modelling one.
- Glue — every EventBridge event, SQS request and Step Functions state transition has its own line item.
- The hidden rows — CloudWatch Logs ingestion, NAT gateway data processing, and cross-AZ transfer. These are the ones that show up in month two.
Then multiply by the things that aren’t one-to-one: retries, fan-out, and how many reads a single user action really triggers. That multiplied number — cost per user action, and eventually cost per customer — is the one worth putting on a dashboard.
A serverless bill is just your architecture, itemised.
The reference shape
The architecture itself is deliberately unexciting. A thin synchronous path does only what the user is waiting for; everything else becomes an event, processed later in batches.
Each piece earns its place for a cost reason as much as an architectural one.
Keep the hot path thin
The API Lambda validates the request, does the minimum reads and writes the response depends on, emits an event, and returns. It never calls another Lambda synchronously: a function waiting on a function is two bills for one unit of work, plus a second cold start on the critical path.
Two settings matter more than people expect:
- Memory is also CPU. Lambda allocates CPU in proportion to memory, so a function at 1,024 MB often finishes more than twice as fast as one at 512 MB — and costs the same or less. Don’t guess; the open-source AWS Lambda Power Tuning tool runs your function across memory sizes and plots cost against duration.
- Arm is cheaper. Graviton (arm64) functions are billed at a lower GB-second rate than x86. For interpreted runtimes the switch is usually a one-line change.
Model the table around access patterns
DynamoDB is cheap when every request is a GetItem or a narrow Query against a key you designed for it, and expensive when anything reaches for a Scan. I list the access patterns first, then design keys so each one is a single-partition query.
A few rules keep the data bill flat:
- Keep hot items small. A 3 KB item costs one read unit; a 5 KB item costs two. Move large, rarely-read attributes — raw payloads, history — into a separate item or S3.
- Count your indexes. Every write to the table is also a write to each global secondary index that projects the item. Four GSIs can mean five times the write cost. Project only the attributes each index actually needs.
- Let TTL delete for you. Expiring items with Time to Live doesn’t consume write capacity, unlike deleting them yourself.
- Pick the capacity mode on evidence. On-demand is the right default for new or spiky workloads. Once traffic is steady and predictable, provisioned capacity with auto scaling is usually cheaper — revisit it after a month of real data rather than on day one.
Push the rest onto events, and batch them
Anything the user isn’t waiting for — notifications, search indexing, analytics, thumbnails — goes onto EventBridge as a fact (“order placed”), and the rules route it to where it’s needed. Filtering in rule patterns means consumers only receive, and only pay to process, the events they care about.
Where volume is high, I put SQS between EventBridge and the worker so Lambda can pull messages in batches. Processing fifty messages in one invocation instead of fifty invocations is the single largest lever on this side of the diagram:
const worker = new lambda.Function(this, 'Worker', {
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64, // cheaper GB-seconds
memorySize: 1024, // measured with Power Tuning
timeout: Duration.seconds(30),
reservedConcurrentExecutions: 20, // a ceiling on spend and on downstream load
logGroup: new logs.LogGroup(this, 'WorkerLogs', {
retention: logs.RetentionDays.TWO_WEEKS, // the default is to keep logs forever
}),
handler: 'worker.handler',
code: lambda.Code.fromAsset('dist/worker'),
});
worker.addEventSource(new SqsEventSource(queue, {
batchSize: 50,
maxBatchingWindow: Duration.seconds(10), // wait for a fuller batch
reportBatchItemFailures: true, // retry only the messages that failed
}));
reportBatchItemFailures matters more than it looks. Without it, one bad message fails the whole batch and all fifty get retried — a cost multiplier hiding inside an error path.
For multi-step workflows, the same thinking applies to Step Functions: Standard workflows are billed per state transition, Express workflows by requests and duration. High-volume, short-lived workflows are usually far cheaper as Express.
The multipliers to hunt down
Most serverless cost incidents I’ve investigated came from one of these, not from the base price of anything:
- Retry storms. Clients retry, SDKs retry, Lambda’s async invocation retries, SQS redelivers. During a downstream outage these stack. Set explicit retry limits at every layer, add jitter, and send poison messages to a dead-letter queue after a few attempts instead of letting them loop.
- Chatty fan-out. One domain event that triggers ten functions, each of which emits more events, grows geometrically. Draw the event graph for your busiest action and count the invocations it causes.
- Log volume. CloudWatch Logs charges for ingestion, and log groups keep data forever by default. Log structured summaries at
INFO, sample the verbose ones, and set a retention period on every group. - NAT gateways. A Lambda attached to a VPC reaches the internet — and AWS APIs — through a NAT gateway that bills per GB processed. Only attach functions to a VPC when they need private resources, and use VPC gateway endpoints for S3 and DynamoDB so that traffic skips the NAT entirely.
- Polling. Anything that wakes up on a schedule to ask “anything new?” costs money even when the answer is no. Prefer events.
Guardrails that make the bill predictable
Architecture keeps the unit cost low. Guardrails keep a bad day from becoming a bad month.
- Reserved concurrency as a circuit breaker. A ceiling on a function’s concurrency is also a ceiling on its spend, and it protects whatever that function talks to. Set it deliberately for every async worker.
- Budgets and anomaly detection. An AWS Budget per account with alerts at 50%, 80% and 100% of forecast, plus Cost Anomaly Detection watching per-service spend, turns “we found out at month-end” into “we found out this afternoon”.
- Tags that map to features. Cost allocation tags for service, feature and environment let you answer “what does search cost us?” instead of “what does Lambda cost us?”. Enforce them in your infrastructure code, not by convention.
- A cost-per-request dashboard. Divide daily spend by daily requests for each major feature and chart it. The absolute bill should grow with traffic; the unit cost shouldn’t. When the line bends upward, something changed — and now you’ll notice the week it happens.
Before a serverless service goes live, I run down this list:
- There’s a written estimate of cost per request, including retries and fan-out.
- The synchronous path is thin, and no Lambda waits on another Lambda.
- Memory is measured, not guessed, and functions run on arm64 where possible.
- DynamoDB keys fit the access patterns, hot items are small, and every GSI earns its write cost.
- Asynchronous work is batched, with partial-batch failure reporting and a dead-letter queue.
- Every log group has a retention period, and VPC-attached functions use gateway endpoints.
- Concurrency limits, budgets and anomaly alerts are in place before traffic arrives.
Get these right and the bill becomes what it should be: a line that grows with your traffic, not ahead of it.