Serverless with Lambda
What Lambda is, a complete function example, and when serverless makes sense versus a regular server.
What Lambda is
AWS Lambda runs a function you write in response to an event, without you provisioning, patching, or paying for a server that sits idle between invocations. "Serverless" doesn't mean there's no server involved — it means the server is entirely AWS's problem: it starts your code, runs it, stops it, and scales the number of concurrent executions up or down automatically based on incoming demand.
The trigger for a Lambda invocation can be almost anything: an HTTP request through API Gateway, a file uploaded to S3, a message arriving on an SQS queue, a scheduled cron-like rule, or a direct SDK/CLI invocation. Lambda runs your code, returns (or forwards) the result, and then the environment is torn down or frozen for potential reuse — there's no long-running process you manage the lifecycle of.
A complete function example
Here's a complete Lambda function (Node.js) that resizes an uploaded image whenever a new file lands in an S3 bucket, and writes the thumbnail to a second bucket:
// index.mjs
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from "sharp";
const s3 = new S3Client({});
const THUMBNAIL_BUCKET = process.env.THUMBNAIL_BUCKET;
export const handler = async (event) => {
const record = event.Records[0];
const sourceBucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
const original = await s3.send(
new GetObjectCommand({ Bucket: sourceBucket, Key: key })
);
const buffer = await original.Body.transformToByteArray();
const thumbnail = await sharp(buffer).resize(200, 200).toBuffer();
await s3.send(
new PutObjectCommand({
Bucket: THUMBNAIL_BUCKET,
Key: `thumbnails/${key}`,
Body: thumbnail,
ContentType: "image/jpeg",
})
);
return { statusCode: 200, key };
};
Deploying it with the CLI — zip the code and its dependencies, then create the function pointing at an execution role (the Lambda equivalent of the EC2 instance role covered on the previous page):
zip -r function.zip index.mjs node_modules
aws lambda create-function \
--function-name generate-thumbnail \
--runtime nodejs20.x \
--handler index.handler \
--role arn:aws:iam::123456789012:role/lambda-thumbnail-role \
--zip-file fileb://function.zip \
--timeout 15 \
--memory-size 256 \
--environment "Variables={THUMBNAIL_BUCKET=my-app-thumbnails}"
# Wire it up to fire whenever an object is created in the source bucket
aws s3api put-bucket-notification-configuration \
--bucket my-app-uploads \
--notification-configuration file://notification.json
The handler value (index.handler) tells Lambda which exported function in which file to call — index.mjs's exported handler. --timeout and --memory-size matter more here than they might for a normal server: Lambda bills by execution duration multiplied by allocated memory, and a function that runs past its configured timeout is killed and reported as an error, not left to keep running.
The pieces that make Lambda work
- Handler — the specific function Lambda calls for each invocation, receiving an
eventobject describing what triggered it (an S3 event, an API Gateway request, an SQS message body) and acontextobject with metadata about the current invocation. - Execution role — an IAM role (see the previous page) Lambda assumes to run your function, granting it exactly the permissions it needs — reading from the source bucket and writing to the destination bucket, in the example above, nothing more.
- Trigger / event source — what actually invokes the function: an S3 bucket notification, an API Gateway route, an SQS queue, an EventBridge scheduled rule, or a direct
aws lambda invokecall. - Cold start — the first invocation after a period of inactivity has to initialize a fresh execution environment (start the runtime, load your code) before running your handler, adding latency that a "warm" invocation (reusing an already-initialized environment) doesn't pay. This is the most commonly cited latency trade-off of serverless compute versus an always-running server.
When serverless makes sense vs. a regular server
| Lambda (serverless) | EC2 / a regular server | |
|---|---|---|
| Billing | Per invocation + execution time; nothing while idle | Per hour/second the instance runs, whether busy or idle |
| Scaling | Automatic, from zero to thousands of concurrent invocations | Manual, or via Auto Scaling Groups you configure |
| Max execution time | Hard limit (15 minutes) | No inherent limit — a process can run indefinitely |
| Startup latency | Cold starts on infrequent/scaled-out invocations | None — the process is already running |
| Operational burden | None — AWS patches and manages the runtime | You patch the OS, manage capacity, handle failover |
| Best fit | Spiky, infrequent, or event-driven workloads | Steady, predictable, long-running, or stateful workloads |
Lambda is the right tool for workloads that are naturally event-shaped and don't run continuously: processing an uploaded file, responding to a webhook, running a nightly cleanup job, handling a moderate-traffic API with unpredictable spikes. It's the wrong tool for a workload that needs to hold long-lived state in memory across requests, run continuously regardless of traffic (a background worker that's always polling something isn't actually saving money as Lambda, since it never goes idle), or exceed the 15-minute maximum execution time. A steady, predictable, always-on workload is very often cheaper on a regular EC2 instance than as constantly-firing Lambda invocations — serverless isn't "always the cheap option," it's the option that avoids paying for idle capacity, which only helps when there actually is meaningful idle time to avoid paying for.
Common mistakes
- Treating Lambda as a place to run a workload that's always busy — if a function is effectively invoked continuously with no idle gaps, an EC2 instance (or a container) is very often cheaper and avoids repeated cold starts entirely.
- Ignoring cold starts for latency-sensitive, user-facing endpoints — a function invoked rarely enough to cool down between requests can add hundreds of milliseconds to an unlucky request; provisioned concurrency exists specifically to keep a configured number of execution environments warm for this case.
- Granting a Lambda's execution role broad permissions "to be safe," rather than exactly what that one function touches — the same least-privilege reasoning from the IAM page applies to execution roles just as much as to EC2 instance roles.
- Writing a function with no timeout margin for a slow dependency (a flaky external API, a cold database connection) — a function killed mid-request by its own timeout is a much messier failure than one that fails cleanly and quickly.