Serverless functions
Run code on demand without managing servers: Lambda, edge functions, triggers and cold starts.
In 30 seconds
Serverless functions let you upload a piece of code and have a cloud provider run it whenever an event happens: an HTTP request, a file upload, a queue message or a schedule. You do not manage servers, operating systems or scaling; the platform starts as many copies as needed and you pay per invocation and run time. The trade-offs are cold starts, time and memory limits, and tighter coupling to one provider.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- Teams use serverless to ship small APIs, webhooks and background jobs without running or patching servers.
- It scales automatically from zero to thousands of concurrent executions, which suits spiky or unpredictable traffic.
- Pay-per-use pricing makes low-traffic or bursty workloads very cheap compared to always-on machines.
- Modern frontend platforms (Vercel, Netlify, Cloudflare) run framework server code such as Next.js API routes as functions, so most web developers already depend on serverless even if they do not call it that.
How it works
01The idea: functions instead of servers
On a classic server, your app runs all the time and you pay for the machine whether it is busy or not. Serverless (more precisely Functions as a Service, FaaS) flips that: you deploy a function, the platform keeps it dormant, and when an event arrives it starts an execution environment, runs your handler and later freezes or discards it. Think of it like a taxi instead of owning a car: no parking, no maintenance, you pay per trip, but each trip has a short wait for pickup and you follow the taxi company's rules. 'Serverless' does not mean no servers; it means servers are the provider's problem. The main platforms are AWS Lambda, Google Cloud Run functions, Azure Functions, and edge platforms such as Cloudflare Workers, Vercel Functions and Netlify Functions.
02How an invocation works under the hood
When an event arrives and no warm environment is free, the platform does a cold start: it provisions a micro-VM (Lambda uses Firecracker) or a V8 isolate (Cloudflare Workers), downloads your code, starts the runtime and runs your initialisation code outside the handler. Then it calls the handler with the event. After the response, the environment stays warm for a while and may serve the next request without the init cost. Each environment handles one request at a time on classic Lambda, so 100 concurrent requests mean 100 environments. Code at module level runs once per environment, which is where you create database clients and load configuration. Anything in memory or /tmp may survive between calls but is never guaranteed.
- Cold start: init + first invoke, often 100 ms to over 1 s depending on runtime and package size.
- Warm start: handler only, usually a few milliseconds of overhead.
- Concurrency: number of environments running at the same moment.
03Writing an AWS Lambda function
A Lambda function is a handler that receives an event and a context. For HTTP, you put it behind a Function URL or API Gateway, which turns the HTTP request into an event object and your return value back into a response. Current managed runtimes include Node.js 24 (nodejs24.x) and Python 3.14 on Amazon Linux 2023, on x86_64 or arm64 (Graviton, usually cheaper). Memory is configurable from 128 MB to 10,240 MB and CPU scales with it; the maximum timeout is 15 minutes; synchronous request and response payloads are limited to 6 MB. Initialise SDK clients outside the handler so warm invocations reuse them.
// index.mjs (runtime: nodejs24.x, handler: index.handler)
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const db = new DynamoDBClient({}); // created once per environment
export const handler = async (event) => {
const id = event.pathParameters?.id;
if (!id) return { statusCode: 400, body: JSON.stringify({ error: "id required" }) };
const res = await db.send(new GetItemCommand({
TableName: process.env.TABLE_NAME,
Key: { pk: { S: `product#${id}` } },
}));
if (!res.Item) return { statusCode: 404, body: "{}" };
return { statusCode: 200, headers: { "content-type": "application/json" },
body: JSON.stringify({ id, name: res.Item.name.S }) };
};
04Event triggers: more than HTTP
The real power of serverless is reacting to events across a cloud. On AWS, a function can run when an object lands in S3, a message arrives in SQS, a row changes in DynamoDB Streams, an EventBridge rule or schedule fires, or a Kinesis stream gets records. Invocations are synchronous (the caller waits, as with HTTP), asynchronous (the platform queues the event and retries on error, with a dead-letter destination), or via event source mappings that poll a queue or stream and deliver batches. Because asynchronous and queue triggers retry, the same event can be processed more than once: handlers must be idempotent. For SQS batches, report partial failures so only failed messages are retried.
// SQS trigger with partial batch failures
// (enable ReportBatchItemFailures on the event source mapping)
export const handler = async (event) => {
const batchItemFailures = [];
for (const record of event.Records) {
try {
const job = JSON.parse(record.body);
await processOnce(job.id, job); // idempotent: skips already-done ids
} catch (err) {
console.error({ messageId: record.messageId, err: String(err) });
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
05Edge functions: code close to the user
Edge functions run in many locations worldwide, near the user, instead of one region. Cloudflare Workers use V8 isolates rather than containers: an isolate starts in milliseconds, so cold starts are nearly invisible, but the environment is a web-standard runtime (fetch, Request, Response, Web Crypto) with limited Node.js APIs and tighter CPU limits. They are ideal for redirects, auth checks, A/B testing, header rewriting, geolocation and lightweight APIs backed by edge storage (KV, D1, R2, Durable Objects). For heavy database work, a far-away database cancels the benefit: an edge function in Sydney calling a database in Frankfurt is slower than a regional function next to the database. Vercel's Functions now default to a Node.js runtime with 'Fluid compute', which lets one instance serve several concurrent requests.
// Cloudflare Worker: geo-based redirect at the edge
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const country = (request as any).cf?.country ?? "US";
if (url.pathname === "/" && country === "DE") {
return Response.redirect(new URL("/de", url).toString(), 302);
}
return fetch(request); // pass through to origin
},
};
06Deploying with infrastructure as code
Clicking functions together in a console does not scale or survive a team change. Define functions, triggers, permissions and environment in code: AWS SAM, the Serverless Framework, AWS CDK, SST or Terraform. Each function should have its own IAM role with only the permissions it needs. Secrets come from Secrets Manager or Parameter Store, not plain environment variables in the repository. Package dependencies carefully: smaller bundles start faster, so bundle with esbuild and exclude the AWS SDK if the runtime already provides it (Node.js runtimes include SDK v3).
# template.yaml (AWS SAM)
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
GetProduct:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs24.x
Architectures: [arm64]
Handler: index.handler
MemorySize: 512
Timeout: 10
Environment: { Variables: { TABLE_NAME: !Ref Products } }
Policies: [ { DynamoDBReadPolicy: { TableName: !Ref Products } } ]
Events:
Api: { Type: HttpApi, Properties: { Path: /products/{id}, Method: get } }
Products:
Type: AWS::Serverless::SimpleTable
07Cost, limits and when it stops being cheap
Lambda bills per request plus GB-seconds (memory times duration, in 1 ms increments), with a monthly free tier. For bursty or low traffic this is far cheaper than an always-on server. For steady, high traffic, a function running constantly can cost more than a container or VPS doing the same work, and API Gateway, NAT gateways and logging often cost more than the function itself. Watch account concurrency limits (1,000 per region by default, raisable), reserved concurrency to protect downstream databases, and timeouts. A useful rule of thumb: serverless is ideal for event glue, webhooks, scheduled jobs and spiky APIs; long-running, connection-heavy or steady workloads fit containers better.
Use it when
- Webhooks, scheduled jobs, file processing and other event-driven glue code.
- APIs with spiky, unpredictable or low traffic where paying for idle servers is wasteful.
- Edge logic such as redirects, auth gates, A/B tests and personalisation that must be fast worldwide.
- Small teams that want managed scaling and patching and accept some provider lock-in.
Skip it when
- Long-running jobs over 15 minutes, WebSocket servers or processes that need persistent in-memory state.
- Steady, high-throughput workloads where an always-on container or VPS is cheaper and more predictable.
- Latency-critical paths where occasional cold starts are unacceptable and provisioned capacity is too costly.
Alternatives
- VPS + Docker Compose
Predictable cost and full control; you handle patching and scaling, but long-running processes and WebSockets are simple.
- Serverless containers (Cloud Run, Fargate, Fly.io)
Run any container with autoscaling, fewer runtime limits than functions.
- Kubernetes
Maximum control for many services; much more operational work.
- Queue workers (BullMQ, Celery)
Background processing on your own servers, with fine control over concurrency and retries.
Best practices
- Initialise clients and config outside the handler, and keep bundles small to reduce cold starts.
- Make every handler idempotent; asynchronous and queue triggers deliver at least once.
- Give each function its own least-privilege IAM role and keep secrets in a secrets manager.
- Set explicit timeouts and memory; test memory sizes, because more memory also means more CPU and can be cheaper overall.
- Use structured JSON logs, tracing (X-Ray or OpenTelemetry) and alarms on errors, throttles and duration.
- Protect databases with connection pooling (RDS Proxy, a serverless driver) and reserved concurrency.
- Define everything in infrastructure as code and deploy through CI.
Common mistakes
- Opening a new database connection per invocation, exhausting the database at the first traffic spike.
- Putting functions in a VPC with a NAT gateway just to call public APIs, adding cost and latency for no reason.
- Assuming in-memory or /tmp state persists between calls, or that an event is delivered exactly once.
- Letting recursive triggers loop (a function writes to the same S3 bucket that triggers it).
- Ignoring the bill for API Gateway, logs and data transfer, which can exceed the function cost.
Deep dive
08Inside the execution environment lifecycle
A Lambda execution environment has three phases. Init: the platform starts extensions, the runtime, and runs your module-level code; this phase has its own 10-second limit. Invoke: the handler runs for each event, one at a time per environment. Shutdown: when the platform recycles the environment, extensions get a short window to flush. Between invokes the environment is frozen, so background work started without await (an un-awaited promise, a timer) may pause mid-flight and resume in a later invoke, which causes confusing bugs. Always await everything before returning, or use response streaming when you want to send bytes early. SnapStart, available for Java, Python and .NET, snapshots the initialised environment and resumes from it, so unique values such as random seeds and connections must be refreshed after restore.
09Concurrency, scaling and throttling
Concurrency is roughly requests per second multiplied by average duration: 200 req/s at 250 ms needs about 50 concurrent environments. Each region has an account-level concurrency quota shared by all functions, so one runaway function can throttle the rest. Reserved concurrency sets both a guaranteed slice and a maximum for a function, protecting fragile downstream systems. Provisioned concurrency keeps a number of environments initialised, removing cold starts for that capacity, at an hourly cost. When throttled, synchronous callers get 429 errors, while asynchronous events are retried for up to six hours by default. For queue triggers, maximum concurrency on the event source mapping is the safer way to limit consumers than reserved concurrency, which can cause messages to bounce back to the queue.
# Cap a function at 50 concurrent executions
aws lambda put-function-concurrency \
--function-name get-product --reserved-concurrent-executions 50
# Keep 10 environments warm on the "live" alias
aws lambda put-provisioned-concurrency-config \
--function-name get-product --qualifier live \
--provisioned-concurrent-executions 10
10Performance tuning: memory, architecture and bundling
On Lambda, CPU is allocated in proportion to memory; at 1,769 MB a function gets the equivalent of one full vCPU. So raising memory often makes CPU-bound functions both faster and cheaper, because duration drops more than price rises. Measure with AWS Lambda Power Tuning, which runs your function at several memory sizes and plots cost against speed. arm64 (Graviton) is priced lower per GB-second and is faster for many workloads. For Node.js, bundle with esbuild, mark the AWS SDK as external, use ES modules with top-level await for initialisation, and avoid importing whole SDKs or ORMs on cold paths. For Python, trim dependencies and avoid heavy imports at module level unless they are needed on every call.
11Security model and common attack surfaces
Each function's IAM execution role is its identity; a compromised function can do anything that role allows, so scope it to specific resources and actions. Event data is untrusted input even when it comes from inside AWS: an S3 object key or SQS body can carry injection payloads. Function URLs with auth type NONE are publicly callable; use AWS_IAM auth, or validate a signature or token yourself. Secrets should be fetched from Secrets Manager or Parameter Store with caching (the AWS Parameters and Secrets Lambda Extension helps), not baked into environment variables visible in the console. Pin dependencies and scan them, because supply-chain risk is concentrated in small bundles that ship directly to production.
Real-world scenarios
A traffic spike took the database down
Problem
A marketing email sent thousands of users to an API built on Lambda and PostgreSQL. Lambda scaled instantly, then the database hit its connection limit and every request failed.
How to solve it
First, set reserved concurrency on the API functions to a number the database can handle, so excess requests get throttled instead of crashing everything. Then put RDS Proxy (or PgBouncer) between the functions and the database to pool and multiplex connections, and make sure clients are created outside the handler. Cache hot read endpoints at the CDN or in Redis so most requests never reach the database. Finally, load-test with realistic concurrency before the next campaign and alarm on throttles and database connection count.
Duplicate emails after a deployment
Problem
An SQS-triggered function sends order confirmation emails. After a slow third-party outage, some customers received the same email three times.
How to solve it
The function was not idempotent: timeouts caused SQS to make messages visible again and redeliver them. Fix it by recording each order ID in DynamoDB with a conditional put (attribute_not_exists) before sending; if the write fails, skip the send. Set the function timeout below the queue's visibility timeout (AWS recommends visibility at least six times the function timeout), enable ReportBatchItemFailures so one failure does not retry the whole batch, and attach a dead-letter queue with an alarm so persistent failures are investigated instead of retried forever.
Image resizing on upload
Problem
A product needs thumbnails generated when users upload photos. The team does not want to run a dedicated worker server for a job that happens a few hundred times a day.
How to solve it
Create an S3 bucket for originals with an event notification on ObjectCreated that triggers a Lambda function. The function streams the original, resizes with a native library such as sharp packaged for arm64, and writes thumbnails to a different bucket or prefix to avoid recursive triggers. Give it 1-2 GB of memory for faster CPU, a 30-second timeout, and an IAM role limited to read from the source and write to the destination. Serve thumbnails through a CDN. Cost stays close to zero at this volume, and there is nothing to patch.
Cheat sheet
- Invoke a deployed function
aws lambda invoke --function-name fn --payload '{}' --cli-binary-format raw-in-base64-out out.json- Tail logs live
aws logs tail /aws/lambda/fn --follow- SAM: build and deploy
sam build && sam deploy --guided- SAM: run locally
sam local invoke GetProduct -e event.json- Limit concurrency
aws lambda put-function-concurrency --function-name fn --reserved-concurrent-executions 50- Create a public HTTPS URL (IAM auth)
aws lambda create-function-url-config --function-name fn --auth-type AWS_IAM- Cloudflare Worker: new and deploy
npm create cloudflare@latest && npx wrangler deploy- Lambda hard limits
15 min timeout, 128 MB-10,240 MB memory, 6 MB sync payload, 10 GB /tmp max
Questions & answers
Q1What is a cold start and how do you reduce it?
A cold start happens when no warm execution environment is available, so the platform must create one, load your code, start the runtime and run your initialisation before handling the request. It can add from about a hundred milliseconds to over a second. To reduce it: keep the deployment package small by bundling and tree-shaking, avoid heavy frameworks in the init path, choose a fast-starting runtime, and lazily load rarely used modules. Where it really matters, use provisioned concurrency to keep environments warm, or SnapStart on supported runtimes to resume from a snapshot. Edge isolate platforms largely avoid cold starts by design.
Q2How do serverless functions talk to a relational database safely?
The danger is connection count: each concurrent environment opens its own connection, so a spike to 500 concurrent invocations can try to open 500 connections and exhaust PostgreSQL or MySQL. Solutions: put RDS Proxy or PgBouncer in front of the database to pool connections; use HTTP-based or serverless drivers offered by providers such as Neon or PlanetScale; create the client outside the handler so warm calls reuse it; and cap concurrency with reserved concurrency so functions cannot overwhelm the database. Also keep queries short; long transactions hold pooled connections and reduce effective capacity.
Q3When is serverless more expensive than a server?
When load is steady and high. Functions are priced per request and per GB-second, which is great when traffic is bursty or idle much of the time, because idle costs nothing. But if a function is effectively busy around the clock, the same work on a container or VPS is usually cheaper. Hidden costs matter too: API Gateway per-request fees, NAT gateway hourly and data charges, CloudWatch log ingestion and cross-region transfer often exceed the compute bill. I estimate with real traffic numbers and compare total cost, not just the function price.
Q4What does 'at-least-once delivery' mean for function design?
Asynchronous invocations, SQS, EventBridge and stream triggers guarantee an event is delivered, but on errors, timeouts or internal retries they may deliver it more than once. So handlers must be idempotent: processing the same event twice must have the same effect as once. Typical techniques are storing processed event IDs with a conditional write, using natural keys and upserts, and passing idempotency keys to payment providers. For failures that keep recurring, configure a dead-letter queue or on-failure destination so poisoned messages are parked for inspection instead of retrying forever.
Q5Edge functions or regional functions: how do you decide?
I look at where the data lives and what the code needs. Edge functions run near the user and start almost instantly, so they are great for redirects, auth token checks, headers, A/B tests, geolocation and responses built from edge storage or cache. But they have restricted runtimes and limited CPU time, and if each request must hit a single-region database, the round trips from distant edges make them slower than a regional function placed next to the database. So: edge for lightweight, data-light logic; regional for anything that is database-heavy or needs full Node.js APIs.
Q6How do you test and debug serverless functions?
I keep business logic in plain modules that are unit-tested without any cloud, and keep handlers thin: parse the event, call the logic, shape the response. For integration, AWS SAM can invoke functions locally with sample events and emulate API Gateway, and LocalStack emulates more services. Then every pull request deploys to a separate stage or account for real end-to-end tests. In production, structured JSON logs with a request ID, distributed tracing and alarms on errors, throttles and duration are essential, because you cannot attach to a server and look around.
Senior-level questions
S1Design a serverless order pipeline that is reliable under partial failures.
The API function validates the order, writes it to DynamoDB with an idempotency key from the client, and publishes an OrderPlaced event in the same step using DynamoDB Streams or a transactional outbox, so the write and the event cannot diverge. EventBridge routes the event to separate SQS queues for payment, inventory and email, each consumed by its own function with partial batch failure reporting and a dead-letter queue. Every consumer is idempotent, keyed by order ID. For multi-step flows with compensation, such as refunding when inventory fails, Step Functions orchestrates the saga with explicit retries and timeouts. Observability uses correlation IDs across all functions, and alarms watch DLQ depth, error rates and age of oldest message.
S2How do you avoid vendor lock-in with serverless, and is it worth it?
Some lock-in is the price of the benefit, so I decide per layer. Business logic lives in plain modules with no provider imports, and handlers are thin adapters, which keeps the expensive part portable. HTTP functions can use web-standard Request and Response through frameworks like Hono that run on Lambda, Workers, Node and Bun. Infrastructure is defined in Terraform or a similar tool so it is at least documented and reviewable. What I do not do is wrap every managed service in a home-grown abstraction; reimplementing queues or event routing to be 'cloud-agnostic' usually costs more than a migration ever would. The real exit plan is containerising the same handlers onto Cloud Run, Fargate or a VPS if pricing or limits change.
S3When would you move a workload off Lambda, and to what?
Signals are: steady high utilisation where GB-second pricing exceeds container cost; functions near the 15-minute limit or needing long-lived connections like WebSockets or gRPC streams; heavy per-request initialisation that cold starts punish; or many functions sharing so much code that deployments become slow and coupled. The usual target is serverless containers such as Fargate or Cloud Run, which keep autoscaling and managed infrastructure but remove runtime limits and allow many concurrent requests per instance, or a simple VPS with Docker Compose for predictable workloads. I would measure first, migrate the hottest path, and keep event-driven glue such as webhooks and scheduled jobs on functions, where they are cheapest.
Glossary
- FaaS
- Functions as a Service: the provider runs your individual functions in response to events and bills per use.
- Cold start
- Extra latency when a new execution environment must be created and initialised before handling a request.
- Execution environment
- The isolated micro-VM or isolate where a function runs; it may be reused for later invocations.
- Concurrency
- How many invocations of a function run at the same moment, each in its own environment.
- Provisioned concurrency
- Pre-initialised environments kept warm for a fee, removing cold starts for that capacity.
- Event source mapping
- A Lambda resource that polls a queue or stream and invokes your function with batches of records.
- Dead-letter queue
- A queue where events that keep failing are parked for later inspection instead of retrying forever.
- V8 isolate
- A lightweight sandbox inside one JavaScript engine process; it starts in milliseconds and powers edge platforms.
- Idempotent
- An operation that has the same effect whether it runs once or several times with the same input.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.