Backend Architecture

Serverless Backend Architecture: The Complete Guide

From AWS Lambda fundamentals to edge computing and cost optimization — everything you need to build production-grade serverless backends in 2026. Practical TypeScript examples, real architecture patterns, and hard-won lessons.

Amit Sharma
Amit SharmaSoftware Engineer, Backend Architecture at TurboDocx
March 24, 202620 min read

Serverless computing has fundamentally changed how we build backends. Instead of provisioning servers, configuring auto-scaling groups, and patching operating systems, you write functions and deploy them. The cloud provider handles everything else — scaling, availability, security patches, and capacity planning. In 2026, serverless is no longer an experiment; it's the default architecture for most new backend workloads.

This guide covers the full serverless landscape: from the fundamentals of Functions-as-a-Service to advanced patterns like Step Functions orchestration, edge computing with Cloudflare Workers, and the cost optimization strategies that determine whether serverless saves you money or burns through your budget. Whether you're building a high-throughput API or a document automation pipeline, these patterns will help you architect backends that scale without operational overhead.

TechnologyCategoryBest ForProvider
AWS LambdaFaaSEvent-driven microservices, API backendsAWS
Cloudflare WorkersEdge FunctionsLow-latency APIs, geo-distributed workloadsCloudflare
Vercel Edge FunctionsEdge FunctionsNext.js middleware, personalizationVercel
Google Cloud FunctionsFaaSFirebase integrations, GCP event handlingGoogle
Step FunctionsOrchestrationMulti-step workflows, saga patternsAWS
EventBridgeEvent BusCross-service event routing, decouplingAWS
API GatewayGatewayREST/WebSocket APIs, auth, rate limitingAWS
DynamoDB StreamsChange Data CaptureReactive data pipelines, event sourcingAWS
Deno DeployEdge FunctionsTypeScript-first edge APIs, global deployDeno
WebAssembly (Wasm)Edge RuntimeCPU-intensive tasks at the edge, portabilityMulti-platform

Part 1: Serverless Fundamentals

FaaS vs BaaS

Serverless is an umbrella term that covers two distinct models. Functions-as-a-Service (FaaS) — AWS Lambda, Google Cloud Functions, Azure Functions — lets you deploy individual functions that execute in response to events. You control the code; the provider controls the runtime. Backend-as-a-Service (BaaS) — Firebase, Supabase, AWS AppSync — provides managed backend capabilities (auth, database, storage) that you consume through APIs without writing any server-side code.

Most production architectures combine both. You use BaaS for commodity operations (user authentication, file storage) and FaaS for custom business logic that doesn't fit a managed service. The key architectural decision is knowing where the boundary lies for your specific use case.

Event-Driven Execution Model

Every serverless function is triggered by an event: an HTTP request, a message on a queue, a file upload to S3, a database change, or a scheduled cron expression. This event-driven model is fundamentally different from traditional servers that sit idle waiting for requests. Functions exist only while processing an event — there is no idle state, which is why the pay-per-use pricing model works.

Understanding event sources is critical for building integrations that respond to real-world triggers. A document upload triggers processing, a webhook from a CRM triggers data enrichment, a scheduled event triggers report generation — each pattern maps naturally to a serverless function.

* Code examples throughout this guide are simplified for illustrative purposes. Refer to the linked official documentation for complete API references and production-ready configurations.

Cold Starts and Warm Pools

When a function hasn't been invoked recently, the cloud provider must spin up a new execution environment: download your code, initialize the runtime, run your module-level initialization, and then execute the handler. This process is a cold start, and it adds 50–500ms of latency depending on the runtime, package size, and whether you're inside a VPC.

After execution, the provider keeps the environment warm for a period (typically 5–15 minutes). Subsequent invocations reuse this environment, skipping the cold start entirely. This is the warm pool. Understanding this lifecycle is essential — it affects how you structure initialization code, manage database connections, and handle cached data.

// AWS Lambda Handler in TypeScript
// Module-level code runs ONCE during cold start, then is reused
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
// Initialize outside the handler — reused across warm invocations
const dynamodb = new DynamoDBClient({ region: "us-east-1" });
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body || "{}");
// Validate input
if (!body.templateId || !body.variables) {
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ error: "templateId and variables are required" }),
};
}
// Process document generation request
const documentId = crypto.randomUUID();
await dynamodb.send(
new PutItemCommand({
TableName: process.env.DOCUMENTS_TABLE!,
Item: {
pk: { S: `DOC#${documentId}` },
templateId: { S: body.templateId },
variables: { S: JSON.stringify(body.variables) },
status: { S: "pending" },
createdAt: { S: new Date().toISOString() },
},
})
);
return {
statusCode: 201,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ documentId, status: "pending" }),
};
};

Docs: AWS SDK v3 DynamoDB | Lambda TypeScript Handler

Key insight: module-level initialization

Code outside the handler function runs once during cold start and is reused for subsequent warm invocations. This is where you should initialize database clients, SDK instances, and cached configuration. Never initialize these inside the handler — you'll pay the connection cost on every invocation instead of amortizing it across the warm pool.

Part 2: Serverless Architecture Patterns

API Gateway + Lambda

The most common serverless pattern: API Gateway receives HTTP requests, validates them, and routes them to Lambda functions. API Gateway handles TLS termination, authentication (JWT, API keys, IAM), request/response transformation, throttling, and usage plans. Your Lambda function only handles business logic.

This pattern is the backbone of most API integrations. When you're designing APIs that need to handle bursty traffic without over-provisioning, API Gateway + Lambda scales from zero to thousands of concurrent requests without any configuration changes.

Event-Driven Pipelines

Instead of synchronous request-response, event-driven pipelines use queues and event buses to decouple producers from consumers. A document upload triggers an S3 event, which invokes a Lambda function, which publishes a message to SQS, which triggers another Lambda function for processing. Each step is independent, retryable, and independently scalable.

// Event-driven document processing pipeline
import { S3Event } from "aws-lambda";
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
const sqs = new SQSClient({ region: "us-east-1" });
// Step 1: S3 upload triggers validation
export const validateDocument = async (event: S3Event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key);
const size = record.s3.object.size;
// Validate file constraints
if (size > 50 * 1024 * 1024) {
console.error(`File too large: ${key} (${size} bytes)`);
continue;
}
// Enqueue for processing
await sqs.send(
new SendMessageCommand({
QueueUrl: process.env.PROCESSING_QUEUE_URL!,
MessageBody: JSON.stringify({
bucket,
key,
uploadedAt: record.eventTime,
action: "process_template",
}),
MessageGroupId: key, // Required for FIFO queues (URL must end in .fifo)
MessageDeduplicationId: key, // Required unless content-based deduplication is enabled
})
);
}
};
// Step 2: SQS triggers processing (separate Lambda)
export const processDocument = async (event: { Records: Array<{ body: string }> }) => {
for (const record of event.Records) {
const message = JSON.parse(record.body);
// Process template, extract variables, generate output
console.log(`Processing: ${message.key}`);
}
};

Docs: AWS SDK v3 SQS

Fan-Out/Fan-In Pattern

When a single event needs to trigger parallel processing, fan-out distributes work across multiple concurrent Lambda invocations. An SNS topic or EventBridge rule can fan out a single event to multiple Lambda functions simultaneously. Fan-in aggregates the results — typically using DynamoDB or S3 as a coordination point.

This pattern is powerful for batch processing: generating hundreds of documents from a single template, processing multiple image sizes, or running parallel API calls to multiple external services.

Step Functions for Orchestration

For multi-step workflows that need error handling, retries, branching, and parallel execution, AWS Step Functions provides a state machine that orchestrates Lambda functions. Instead of chaining Lambdas directly (which creates tight coupling and makes error handling a nightmare), Step Functions gives you a visual workflow with built-in retry policies, timeout handling, and audit logging.

// Step Functions state machine definition (CDK)
import { Duration } from "aws-cdk-lib";
import * as sfn from "aws-cdk-lib/aws-stepfunctions";
import * as tasks from "aws-cdk-lib/aws-stepfunctions-tasks";
// Define individual steps
const validateInput = new tasks.LambdaInvoke(this, "ValidateInput", {
lambdaFunction: validateFn,
outputPath: "$.Payload",
});
const generateDocument = new tasks.LambdaInvoke(this, "GenerateDocument", {
lambdaFunction: generateFn,
outputPath: "$.Payload",
retryOnServiceExceptions: true,
});
const convertToPdf = new tasks.LambdaInvoke(this, "ConvertToPDF", {
lambdaFunction: convertFn,
outputPath: "$.Payload",
});
const sendNotification = new tasks.LambdaInvoke(this, "SendNotification", {
lambdaFunction: notifyFn,
});
// Error handler
const handleError = new tasks.LambdaInvoke(this, "HandleError", {
lambdaFunction: errorHandlerFn,
});
// Add error handling to each task
validateInput.addCatch(handleError, { errors: ["States.ALL"] });
generateDocument.addCatch(handleError, { errors: ["States.ALL"] });
sendNotification.addCatch(handleError, { errors: ["States.ALL"] });
// Compose the workflow
const definition = validateInput
.next(generateDocument)
.next(
new sfn.Choice(this, "NeedsPDF?")
.when(sfn.Condition.booleanEquals("$.outputFormat", true), convertToPdf)
.otherwise(new sfn.Pass(this, "SkipConversion"))
)
.next(sendNotification);
new sfn.StateMachine(this, "DocumentWorkflow", {
definitionBody: sfn.DefinitionBody.fromChainable(definition),
timeout: Duration.minutes(10),
});

Docs: AWS CDK Step Functions

Anti-pattern: Lambda-to-Lambda chaining

Never invoke a Lambda directly from another Lambda using the SDK. This creates tight coupling, double-billing (the caller waits while the callee runs), and makes error handling brittle. Use Step Functions for orchestration, SQS for async handoffs, or EventBridge for event-based communication instead.

How we use this at TurboDocx

Our document generation API uses an event-driven pipeline behind the scenes. When a developer calls our SDK to generate a document, the request enters API Gateway, a Lambda function validates the template and variables, and a Step Functions workflow orchestrates the generation, format conversion, and delivery steps. This architecture lets us handle burst traffic during end-of-quarter proposal rushes without any capacity planning.

Part 3: Edge Computing

Edge computing moves your code from centralized data centers to locations physically close to your users. Instead of a request traveling from Tokyo to us-east-1 (200ms+ round trip), it executes at a point of presence in Tokyo (sub-10ms). The trade-off: edge runtimes are more constrained than full Lambda environments, but for the right workloads, the latency reduction is transformative.

Cloudflare Workers

Cloudflare Workers run on the V8 isolate model — no cold starts, sub-millisecond startup, deployed to 300+ edge locations globally. They're ideal for API routing, authentication middleware, A/B testing, and request transformation. The 128MB memory limit and 30-second CPU time limit mean they're not suited for heavy computation, but for the vast majority of API workloads, the constraints don't matter.

Vercel Edge Functions & Deno Deploy

Vercel Edge Functions are purpose-built for Next.js middleware — authentication checks, geo-routing, feature flags, and response rewriting that runs before your page renders. Deno Deploy takes a TypeScript-first approach with native fetch, Request, and Response APIs, zero configuration, and global deployment in seconds.

WebAssembly at the Edge

WebAssembly (Wasm) extends what's possible at the edge. Instead of being limited to JavaScript, you can compile Rust, Go, or C++ to Wasm and run it in edge environments. This unlocks CPU-intensive tasks — image processing, PDF manipulation, data compression — at edge latency. Both Cloudflare Workers and Fastly Compute support Wasm natively.

// Edge function with Hono framework on Cloudflare Workers
import { Hono } from "hono";
import { cors } from "hono/cors";
import { jwt } from "hono/jwt";
import { cache } from "hono/cache";
const app = new Hono();
// Middleware: CORS, JWT auth, caching
app.use("/*", cors({ origin: "https://app.turbodocx.com" }));
app.use("/api/*", jwt({ secret: "your-jwt-secret" }));
// Geo-based routing — runs at the edge, sub-10ms response
app.get("/api/config", cache({ cacheName: "config", cacheControl: "max-age=300" }), (c) => {
const country = c.req.header("CF-IPCountry") || "US";
const config = {
region: country,
apiEndpoint: getRegionalEndpoint(country),
features: getFeatureFlags(country),
currency: getCurrency(country),
};
return c.json(config);
});
// API proxy with edge caching and transformation
app.get("/api/templates/:id", async (c) => {
const templateId = c.req.param("id");
const cacheKey = `template:${templateId}`;
// Check edge KV cache first
const cached = await c.env.TEMPLATES_KV.get(cacheKey, "json");
if (cached) return c.json(cached);
// Fetch from origin, cache at edge
const response = await fetch(`${c.env.ORIGIN_API}/templates/${templateId}`);
const template = await response.json();
await c.env.TEMPLATES_KV.put(cacheKey, JSON.stringify(template), { expirationTtl: 3600 });
return c.json(template);
});
function getRegionalEndpoint(country: string): string {
const regionMap: Record<string, string> = {
US: "https://api-us.turbodocx.com",
GB: "https://api-eu.turbodocx.com",
DE: "https://api-eu.turbodocx.com",
JP: "https://api-ap.turbodocx.com",
AU: "https://api-ap.turbodocx.com",
};
return regionMap[country] || regionMap.US;
}
export default app;

Docs: Hono | Cloudflare Workers

Use edge functions for

Auth middleware, geo-routing, API proxies, A/B testing, feature flags, response transformation, rate limiting, and cached API responses.

Keep at origin for

Database writes, long-running computations, file processing over 128MB, workflows requiring VPC access, and operations needing full Node.js APIs.

Edge + origin: the hybrid approach

The best architectures use edge functions as a smart layer in front of origin services. The edge handles auth, caching, and routing; the origin handles business logic and data persistence. This is how platforms like TurboDocx Writer deliver fast global performance while keeping data processing in controlled environments.

Part 4: Cold Start Optimization

Cold starts are the most common objection to serverless adoption, and for latency-sensitive APIs, they're a legitimate concern. But cold starts are not inevitable — they're an optimization problem with well-understood solutions. Here are the strategies that make the biggest difference.

Provisioned Concurrency

AWS Lambda Provisioned Concurrency pre-initializes a specified number of execution environments that are always ready to respond. It eliminates cold starts entirely for those instances. The trade-off is cost — you pay for provisioned environments whether they're handling requests or not. Use it for your latency-critical paths (API endpoints serving UI), not for background processing.

Minimizing Bundle Size

The single biggest lever for cold start reduction is your deployment package size. Every megabyte of code that Lambda needs to load adds latency. Use esbuild or tsup to tree-shake and bundle your TypeScript. Audit your dependencies ruthlessly — a single import from lodash can pull in the entire library if you don't use path imports.

Graviton3 ARM Runtime

AWS Graviton3 (ARM) Lambda functions offer 20% better price-performance than x86 equivalents. They also cold-start faster due to the simpler instruction set. Switching is typically a one-line configuration change with no code modifications required — unless you have native dependencies compiled for x86.

// Optimized Lambda configuration (AWS CDK)
import { Duration } from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
const optimizedFunction = new nodejs.NodejsFunction(this, "OptimizedHandler", {
entry: "src/handlers/api.ts",
handler: "handler",
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64, // Graviton3: 20% cheaper, faster cold start
memorySize: 1024, // More memory = more CPU = faster init
timeout: Duration.seconds(10),
// Bundle optimization: tree-shake, minify, externalize AWS SDK v3
bundling: {
minify: true,
sourceMap: false,
treeShaking: true,
format: nodejs.OutputFormat.ESM,
mainFields: ["module", "main"],
externalModules: ["@aws-sdk/*"], // AWS SDK v3 is included in Lambda runtime
},
// Environment: lazy-load what you can
environment: {
NODE_OPTIONS: "--enable-source-maps",
POWERTOOLS_SERVICE_NAME: "document-api",
POWERTOOLS_LOG_LEVEL: "INFO",
},
// Provisioned concurrency for latency-critical endpoints
currentVersionOptions: {
provisionedConcurrentExecutions: 5,
},
});
// Auto-scaling provisioned concurrency based on utilization
const alias = optimizedFunction.addAlias("live");
const scaling = alias.addAutoScaling({ minCapacity: 5, maxCapacity: 50 });
scaling.scaleOnUtilization({ utilizationTarget: 0.7 });

Docs: AWS CDK NodejsFunction

Cold start optimization checklist

  • Use esbuild/tsup to bundle and tree-shake — target under 5MB zipped
  • Externalize AWS SDK v3 (included in Lambda runtime)
  • Switch to ARM64 (Graviton3) for 20% cost savings
  • Initialize SDK clients at module scope, not inside the handler
  • Use provisioned concurrency for sub-100ms p99 latency requirements
  • Avoid VPC unless you genuinely need private network access
  • Set memory to 1024MB+ — more memory means more CPU and faster init

Part 5: Cost Optimization

Pay-Per-Use Pricing Model

Serverless pricing is based on three dimensions: number of invocations, execution duration (in GB-seconds), and memory allocated. AWS Lambda's free tier includes 1 million requests and 400,000 GB-seconds per month — enough for most development and low-traffic production workloads to run at zero cost.

The key insight: serverless is dramatically cheaper at low volume and competitive at medium volume, but can become more expensive than containers at very high sustained throughput. The crossover point depends on your function's memory usage, execution time, and traffic patterns.

Right-Sizing Memory Allocation

Lambda allocates CPU proportionally to memory. A function with 128MB gets a fraction of a vCPU; a function with 1,769MB gets a full vCPU. Counter-intuitively, doubling memory can reduce your cost because the function executes in less than half the time. Use AWS Lambda Power Tuning (an open-source tool) to find the optimal memory setting for each function.

Caching Strategies

The cheapest Lambda invocation is the one that never happens. Use CloudFront or API Gateway caching for read-heavy endpoints. Use ElastiCache (Redis) or DynamoDB DAX for database query caching. For template rendering, cache compiled templates so repeated generations with different variables skip the parsing step entirely.

Serverless vs Containers: When to Choose What

The decision isn't binary. Most production architectures use both. Here's how they compare across the factors that matter.

FactorServerless (Lambda)Containers (ECS/K8s)
Idle Cost$0 (pay-per-invocation)$50-200/mo minimum (always-on)
10K requests/day~$3-5/month~$50-100/month
1M requests/day~$150-300/month~$200-400/month
10M requests/day~$1,500-3,000/month~$800-1,500/month
Cold Start Latency50-500ms (optimizable)None (always warm)
Scaling SpeedInstant (milliseconds)30-120 seconds
Ops OverheadNear-zeroModerate (K8s, ECS)
Max Execution Time15 min (Lambda)Unlimited

The crossover rule of thumb

Below 5 million requests per day with variable traffic patterns, serverless is almost always cheaper. Above 10 million sustained requests per day, containers typically win on cost. Between 5–10 million, run the numbers with AWS Pricing Calculator for your specific workload. Many MSP teams find serverless ideal for their bursty, multi-tenant workloads.

Key Takeaways

Start Serverless-First

Default to serverless for new workloads. The zero-idle-cost model and instant scaling eliminate entire categories of infrastructure concerns. Only move to containers when you hit genuine limitations.

Optimize Cold Starts Early

Cold starts compound across your architecture. Use provisioned concurrency for latency-critical paths, minimize bundle sizes, and choose ARM runtimes for faster initialization and lower cost.

Edge Is the New Default

For read-heavy APIs, authentication middleware, and personalization logic, edge functions eliminate round-trips to origin servers. Cloudflare Workers and Vercel Edge Functions deploy globally in seconds.

Orchestrate, Don't Chain

Use Step Functions or Temporal for multi-step workflows instead of Lambda-to-Lambda chaining. Orchestrators give you retries, timeouts, parallel execution, and visual debugging for free.

Right-Size Your Architecture

Serverless wins below ~5M requests/day. Above that threshold, run the numbers. Containers become cost-effective for sustained, predictable workloads. Most teams need a hybrid approach.

Design for Events

Serverless architectures thrive on event-driven design. Use EventBridge for cross-service communication, DynamoDB Streams for change data capture, and SQS for reliable async processing.

Applying Serverless to Document Automation

Document generation is a natural fit for serverless architecture. Each document request is an isolated event with clear input (template + variables) and output (generated document). The workload is inherently bursty — sales teams generate proposals in waves at quarter-end, not at a constant rate. Serverless handles this without any capacity planning.

At TurboDocx, our API and SDK abstract away the serverless infrastructure entirely. Developers call a simple endpoint to generate documents from templates, and we handle the event-driven pipeline, cold start optimization, and edge caching behind the scenes. Whether you're building an integration that triggers document generation from a CRM webhook or a custom workflow that generates hundreds of contracts in parallel, the serverless patterns in this guide are exactly what powers the platform underneath.

Related Resources

Build Serverless Document Pipelines with TurboDocx

Our API handles the serverless infrastructure so you can focus on building great products. Generate documents at scale with zero server management.

Amit Sharma
Amit SharmaSoftware Engineer, Backend Architecture at TurboDocx