Most of the backend work we do starts from the same default stack: Node.js, running on AWS Lambda, with SQS handling queues between services and DynamoDB as the primary data store. It’s not the only stack we use, but it’s the one we reach for first, and it’s worth explaining why.
Why serverless first
A traditional always-on server is the right call when you have steady, predictable traffic. Most of what we build doesn’t look like that — traffic is bursty, a lot of the work is triggered by events (a file upload, a queued job, a webhook), and paying for idle compute between those events doesn’t make sense for a small team’s budget or a client’s.
Lambda functions only run — and only cost money — when there’s actually work to do. SQS sits between services as a durable buffer, so a spike in incoming work doesn’t take anything down; it just queues until it’s processed. DynamoDB scales its throughput independently of any single function, so the database isn’t the bottleneck when a Lambda scales out.
What changes about how you write the code
Serverless isn’t just “the same app, hosted differently.” A few things genuinely change:
- Statelessness is enforced, not optional. A Lambda function can’t assume anything persists between invocations. Session state, in-memory caches, connection pools — all of that has to live somewhere else (Redis, DynamoDB, or the request itself).
- Cold starts matter. The first invocation after a period of inactivity pays a startup penalty. Keeping function bundles small and dependencies lean isn’t a nice-to-have here, it directly affects response times.
- Queues change your error handling. With SQS in the middle, a failure isn’t the end of the request — it’s a message that gets retried, and eventually moved to a dead-letter queue if it keeps failing. You design for retries from the start, not as an afterthought.
The failure mode you’re designing around isn’t “the server crashed,” it’s “this specific unit of work failed and needs to be retried without duplicating side effects.”
A minimal shape
A typical function in this stack looks something like this:
exports.handler = async (event) => {
for (const record of event.Records) {
const payload = JSON.parse(record.body);
await processJob(payload); // idempotent — safe to retry
}
};
The important detail isn’t the code itself — it’s that processJob has to be safe to run twice. SQS guarantees at-least-once delivery, not exactly-once, so idempotency is a design requirement, not a nice-to-have.
Where this stack doesn’t fit
Serverless isn’t the right default for everything. Long-running processes, workloads with steady round-the-clock traffic, or anything latency-sensitive enough that cold starts are unacceptable — those usually point toward a more traditional always-on service instead. Knowing when not to reach for this stack is as much a part of the recipe as the stack itself.