Serverless architecture lets you focus on code, not infrastructure. Learn how to build a scalable SaaS backend.
For a SaaS starter that means API + auth + data that scales to zero, then scales automatically. Below is the minimal pattern we use to start: Lambda + API Gateway + DynamoDB, with CloudFront in front.
What is Serverless?
Serverless computing means you don't manage servers. You write functions that run in response to events, and the cloud provider handles scaling.
The minimal SaaS pattern: Lambda + API Gateway + DynamoDB
Client -> CloudFront -> API Gateway -> Lambda -> DynamoDB. Auth at API Gateway. Static assets on CloudFront. Background jobs on EventBridge + Lambda. Keep functions small, one purpose per route, shared validation.
How the pieces fit:
- CloudFront sits in front for static assets and repeatable GETs, so repeated reads do not always reach your functions.
- API Gateway is the auth boundary: verify the caller there, then pass a trusted tenant id to Lambda.
- Lambda stays small and single-purpose per route, with shared validation so every route rejects bad input the same way.
- DynamoDB holds tenant-partitioned rows, and EventBridge plus Lambda handles async work like emails, reports, and syncs outside the request path.
Related: Services for build scope, Functional AI demos for working examples.

Request flow: CloudFront caches repeatable GETs and static assets, API Gateway checks auth, Lambda functions handle one route each and read tenant-partitioned rows in DynamoDB; scheduled work runs on EventBridge + Lambda.
Minimal code — one read route
The smallest useful SaaS read is a tenant-scoped lookup by id. This Node.js Lambda handler uses the AWS SDK for JavaScript v3, reads the table name from TABLE, and returns JSON with explicit 400/404 handling. Add an auth check in production — never trust a client-supplied tenant id alone.
// GET /tenants/{id} — Node.js Lambda + DynamoDB
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async (event) => {
const id = event.pathParameters?.id;
if (!id) return { statusCode: 400, body: JSON.stringify({ error: "missing id" }) };
const tenantId = event.requestContext?.authorizer?.tenantId ?? "public";
const res = await ddb.send(new GetCommand({
TableName: process.env.TABLE,
Key: { pk: `TENANT#${tenantId}`, sk: `PROFILE#${id}` }
}));
if (!res.Item) return { statusCode: 404, body: JSON.stringify({ error: "not found" }) };
return { statusCode: 200, body: JSON.stringify(res.Item) };
};Notes: the tenant-prefixed key (TENANT# + PROFILE#) isolates SaaS tenants in one table, TABLE is set per stage so staging and production never share data, 400/404 stay JSON for the front end, and the authorizer value must be verified against current API Gateway docs at build time — snippet follows the AWS SDK v3 DynamoDBDocumentClient shape, verified syntactically on Node v20.
Cold starts and cost — assumptions first
Cost depends on region, memory, duration, requests, and transfer. Verify current AWS pricing and docs at implementation; this guide publishes no price numbers without a date.
| Choice | When it helps | Trade-off |
|---|---|---|
| 128–256MB, short handler | Webhooks, CRUD | Slower per call |
| 512–1024MB right-sized | Latency-sensitive API | Higher GB-sec, fewer ms |
| Provisioned concurrency | Stable traffic, no cold start | Pay while idle |
| CloudFront cache GETs | Repeatable reads | Invalidation complexity |
What we keep from the original checklist:
- Use provisioned concurrency for consistent workloads
- Implement caching with CloudFront
- Right-size memory allocations
- Monitor with CloudWatch
Keep warm paths lean, give latency-sensitive routes more memory only after measuring, and use provisioned concurrency only for stable routes that truly cannot tolerate a cold start.
*Last verified 2026-09-11 — AWS Lambda, API Gateway, DynamoDB, CloudFront.*
Cost Optimization
The four levers above are the whole cost story at the start: pay while idle only where it removes a real latency problem, cache what repeats, size memory from traces, and watch CloudWatch for duration and error spikes before changing anything else. In practice that means starting with small memory for webhooks and CRUD, measuring latency and GB-sec together for the routes users feel, and adding provisioned concurrency or a CloudFront cache rule only when traces show a repeated, measurable win.
When to move to containers — checklist
Move part or all to containers if: runs longer than 15 minutes, persistent websockets, special binaries or GPU, a 24/7 baseline that is cheaper than provisioned concurrency, or VPC and throughput limits you actually hit. Otherwise stay serverless. Extract the hot service first instead of rewriting everything.
Scope it: Build my estimate
Next step with Next.js
If your front end is Next.js, compare execution options in the Server Actions forms guide, and keep heavy SaaS logic in this Lambda API where auth, tenant isolation, and background jobs are easier to reason about.
Frequently Asked Questions
Is serverless cheaper?
Only for spiky or early traffic; steady high traffic can be cheaper on containers — measure GB-sec plus requests plus transfer on your own workload before deciding.
How do I avoid cold starts?
Keep warm paths lean, right-size memory from traces, and use provisioned concurrency only for stable routes that cannot tolerate a cold start.
How do I multi-tenant safely?
Use tenant-prefixed keys plus an authorizer check on every call, and a separate table per environment. Never accept a tenant id from the client without verifying it at the gateway.
Conclusion
Serverless is powerful for SaaS startups. Start serverless, migrate to containers when needed, and let AWS handle the infrastructure. If you need clarity on scope, Send a brief.
