AWS Lambda Basics: Serverless Functions
A beginner-friendly tour of AWS Lambda — the handler signature, runtime choices, triggers from API Gateway and S3 and EventBridge, cold starts, packaging, and the IAM execution role every function needs.
What you'll learn
- ✓What a Lambda function actually is and the handler signature
- ✓Picking a runtime — Node.js, Python, or one of the others
- ✓How triggers (API Gateway, S3, EventBridge) actually invoke the function
- ✓Cold starts: what they are, when to worry, how to soften them
- ✓Packaging code with zip uploads and container images
- ✓The IAM execution role — the permissions every function needs
Prerequisites
- •An AWS account and a basic mental model — see What Is AWS?
- •Comfort writing a tiny function in Node.js or Python
AWS Lambda runs your code without you running a server. You hand AWS a function and a trigger; AWS handles the machines, the scaling, the patching, the dying-when-idle. For event-driven workloads — webhooks, image resizing, scheduled jobs, glue between services — it is the cheapest and simplest building block in the AWS catalogue.
What Lambda is, mechanically
A Lambda function is a small piece of code that AWS runs in response to an event. You write the code once, configure a trigger (an HTTP request, a file landing in S3, a cron schedule), and pay only for the milliseconds the function actually runs.
Behind the scenes, AWS keeps a pool of micro-VMs (Firecracker) ready to execute your code. When an event arrives:
- AWS picks a warm VM, or starts a new one (a cold start).
- Your code is loaded into a runtime — Node.js, Python, Java, etc.
- The runtime invokes your handler function with the event payload.
- Your code runs, returns a response, and the VM goes back into the pool.
You never touch any of this. You upload a function; AWS does the rest.
The handler signature
Every Lambda runtime has a tiny convention for the entry point. In Node.js, the handler is an async function exported by name:
// index.mjs
export const handler = async (event, context) => {
console.log("event:", JSON.stringify(event));
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from Lambda" }),
};
};
In Python:
# lambda_function.py
import json
def lambda_handler(event, context):
print("event:", json.dumps(event))
return {
"statusCode": 200,
"body": json.dumps({"message": "Hello from Lambda"}),
}
Two arguments arrive every time:
event— the trigger payload, shaped by whichever service invoked the function. An API Gateway event has a request body; an S3 event has bucket and object keys; an EventBridge event has whatever fields the rule defined.context— Lambda runtime metadata: remaining execution time, the request ID, the function name, etc.
The return value is sent back to the caller. For HTTP triggers it should match the { statusCode, headers, body } shape API Gateway expects.
Runtime selection
Lambda supports many runtimes out of the box:
- Node.js (current LTS, plus one older version) — the most common choice for web-style work
- Python (3.x) — also widely used, especially for data and scripting
- Java, .NET, Go, Ruby — first-class
- Custom runtimes — for Rust, Bash, or anything else, via the Lambda Runtime API
- Container images — bring your own image (up to 10 GB) instead of a zip
The choice mostly comes down to your team’s language and any libraries you need. Node.js and Python have the smallest cold starts; Java has the largest by default (mitigable with SnapStart).
Triggers: where events come from
A Lambda function on its own does nothing. You connect it to one or more event sources. The big three:
API Gateway / Function URLs. Turns a Lambda into an HTTP endpoint. Function URLs are the simplest — they give you a direct https://<id>.lambda-url.<region>.on.aws/ URL with no API Gateway in the path. API Gateway adds routing, auth, request validation, and usage plans.
S3 events. Files landing in (or being deleted from) an S3 bucket invoke the function. Classic use cases: generate thumbnails when an image is uploaded, scan documents for viruses, kick off a data pipeline when a CSV arrives.
{
"Records": [
{
"eventName": "ObjectCreated:Put",
"s3": {
"bucket": { "name": "acme-uploads" },
"object": { "key": "users/123/avatar.png" }
}
}
]
}
EventBridge. AWS’s event bus. Use it for cron schedules (rate(5 minutes) or cron(0 9 * * ? *)), reacting to AWS service events (an EC2 instance state change, a CodePipeline failure), or routing custom application events.
Others worth knowing: SQS queues (Lambda polls and processes messages), DynamoDB Streams (react to table changes), Kinesis (streaming data), and Cognito (auth lifecycle hooks).
A first function in the console
The fastest end-to-end is the AWS console:
- Lambda → Create function → Author from scratch.
- Name it, pick Node.js 20.x (or Python 3.12), leave the rest at defaults.
- AWS creates an IAM execution role automatically.
- Replace the inline code with the handler from earlier.
- Click Deploy.
- Click Test, accept the default
hello-worldtemplate, and run it.
The result panel shows the returned JSON, the execution duration, the memory used, and the log output. The whole loop takes maybe a minute.
For a real HTTP endpoint, enable a Function URL under the Configuration tab. You now have a public URL you can curl.
curl https://abc123xyz.lambda-url.us-east-1.on.aws/
# {"message":"Hello from Lambda"}
The IAM execution role
Every Lambda function runs as a particular IAM role — its execution role. The role determines what AWS APIs the function can call. By default, AWS creates one with permission to write CloudWatch Logs and nothing else. Need to read an S3 bucket from inside the function? Attach an S3 read policy to the role.
A minimal role for a function that reads from one bucket might be:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::acme-uploads/*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
The principle of least privilege applies: grant only what the function actually needs. The default AWSLambdaBasicExecutionRole covers logs; everything else you add deliberately.
Cold starts
A cold start is the latency added when AWS spins up a new execution environment because no warm one is available. The runtime initializes, your code is loaded, top-level import statements run, and only then does the handler get called.
Cold start sizes, very roughly:
- Node.js, Python: 100–400 ms
- Java, .NET: 500–2000 ms without tuning
- Container images: bigger images take longer, sometimes seconds
After the first invocation, the environment stays warm for a while (anything from a few minutes to longer) and subsequent invocations are fast. For most workloads — webhooks, async processing, batch jobs — cold starts are not worth worrying about. For latency-sensitive APIs, they are.
Mitigations:
- Provisioned concurrency. Keep N environments permanently warm. Costs money even when idle.
- Smaller bundles. Less code to load means faster cold starts. Tree-shake, avoid huge dependencies.
- SnapStart (Java, Python, .NET). AWS snapshots the initialized environment and restores from it — turns multi-second cold starts into tens of milliseconds.
- Move heavy init outside the handler. Top-level code runs once per container; handler code runs every invocation. Cache database clients, AWS SDK clients, and config in module scope.
import { S3Client } from "@aws-sdk/client-s3";
// runs once per container (init phase)
const s3 = new S3Client({});
// runs every invocation
export const handler = async (event) => {
// use s3 here
};
Try it yourself. Create a Node.js function with a Function URL. Curl it once and note the duration in CloudWatch Logs — that is your cold start. Curl it again immediately — much faster. Wait an hour, curl it again — cold start returns. Now move a new S3Client() outside the handler and watch the per-invocation latency drop.
Packaging: zip vs container
Two ways to ship code to Lambda.
Zip uploads. The classic approach. Bundle your code and node_modules (or your Python site-packages) into a .zip and upload it. Hard limit: 250 MB unzipped. Fast cold starts. Great for small handlers.
# Node.js
npm ci --omit=dev
zip -r function.zip index.mjs node_modules
aws lambda update-function-code \
--function-name hello \
--zip-file fileb://function.zip
Container images. Build a Docker image based on an AWS-provided base image, push it to ECR, and point your function at the image URI. Hard limit: 10 GB. Slightly slower cold starts but supports any toolchain or native binary you want.
FROM public.ecr.aws/lambda/nodejs:20
COPY package*.json ./
RUN npm ci --omit=dev
COPY index.mjs ./
CMD ["index.handler"]
For most beginner functions, the zip path is faster and simpler. Reach for containers when you have native dependencies (ImageMagick, FFmpeg, Playwright) that do not fit in 250 MB.
Limits worth memorising
Lambda has limits, and you will eventually hit them:
- Timeout: up to 15 minutes per invocation. For longer work, use Step Functions or a real server.
- Memory: 128 MB to 10 GB. CPU scales with memory; doubling memory roughly doubles CPU.
- Payload size: 6 MB sync, 256 KB async. Larger? Stash it in S3 and pass a key.
- Concurrent executions: 1000 per account by default. Lambda scales horizontally up to this limit and queues or rejects beyond it.
- /tmp: 512 MB by default (configurable up to 10 GB). Useful for temporary files.
Cost
Lambda’s pricing is two numbers: requests and GB-seconds.
- Requests: $0.20 per million.
- Compute: ~$0.0000166667 per GB-second.
A function with 512 MB of memory that runs for 200 ms costs about 0.5 * 0.2 = 0.1 GB-seconds, or roughly $0.00000167 per call. Even at a million calls a day, you are looking at single-digit dollars per month — plus the free tier (1M requests and 400k GB-seconds per month, indefinitely).
Real costs creep in from chatty integrations: a Lambda that wakes up every minute to poll an empty queue is wasted money compared to an event-driven trigger.
Try it yourself. Wire up an S3 trigger: create a bucket, write a function whose handler logs event.Records[0].s3.object.key, and add the bucket as a trigger in the Lambda console. Upload a file with aws s3 cp test.txt s3://your-bucket/. Open CloudWatch Logs and see your function fire with the key. You just built an event-driven workflow with zero servers.
When not to use Lambda
Lambda is excellent for short, event-driven work. It is a poor fit when:
- You need request latency under 50 ms with no cold starts and do not want to pay for provisioned concurrency.
- You need long-running processes (WebSocket servers, background workers running for hours).
- You need persistent local state (Lambda VMs are ephemeral and shared).
- You are running steady, high-throughput traffic where a few EC2 instances or ECS tasks would actually be cheaper.
In those cases, a container on ECS Fargate or an EC2 instance is often the simpler answer — see AWS EC2 Basics.
Recap
You now know:
- A Lambda function is a handler invoked by AWS in response to an event
- The runtime ecosystem covers Node.js, Python, Java, .NET, Go, Ruby, and container images
- Triggers (API Gateway / Function URLs, S3, EventBridge, SQS, more) shape the
eventpayload - Every function has an IAM execution role — grant least privilege
- Cold starts matter for latency-sensitive APIs; provisioned concurrency, SnapStart, and lean bundles help
- Lambda is cheap per call with a generous free tier, but a poor fit for long-running or steady-state workloads
Lambda is the quickest way to put a piece of code in production on AWS. Once you have shipped one, the patterns repeat for every event-driven job you ever write.
Next steps
Lambdas usually need somewhere to read from and write to. S3 is the most common companion — pair this post with the S3 basics to start building real workflows.
Useful adjacent reading:
- AWS S3 Basics — for storing the inputs and outputs of your functions
- AWS EC2 Basics — when Lambda is the wrong tool
- Deploy to Production with GitHub Actions — for shipping Lambdas from CI
Questions or feedback? Email codeloomdevv@gmail.com.