AWS Lambda and Serverless: A Developer's Practical Guide
Serverless computing fundamentally changes how you think about infrastructure. Instead of managing servers, you deploy functions that run on demand, scale automatically, and charge you only for what you use. AWS Lambda is the leading serverless compute service β and understanding it is increasingly expected for any cloud-aware developer.
What Is Serverless?
"Serverless" does not mean no servers β it means you do not manage them. The cloud provider handles:
- Provisioning and scaling infrastructure
- Patching and maintaining the OS
- Capacity planning
You just write code. You pay per request and execution duration, not for idle server time.
AWS Lambda Basics
A Lambda function is a piece of code that runs in response to an event. AWS manages the underlying compute.
javascript// Node.js Lambda handler export const handler = async (event, context) => { console.log("Event:", JSON.stringify(event)); const name = event.queryStringParameters?.name ?? "World"; return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: `Hello, ${name}!` }), }; };
The handler receives:
- event β the trigger data (HTTP request, S3 event, SQS message, etc.)
- context β runtime info (function name, remaining time, request ID)
- Returns a response appropriate for the trigger type
Lambda Triggers
Lambda functions respond to dozens of event sources:
| Trigger | Use case |
|---|---|
| API Gateway / Function URL | HTTP API endpoints |
| S3 | Process files on upload |
| SQS | Process messages from a queue |
| SNS | React to notifications |
| DynamoDB Streams | React to database changes |
| EventBridge | Scheduled tasks (cron jobs) |
| Cognito | User authentication hooks |
| CloudFront | Edge computing |
HTTP API with API Gateway
yaml# serverless.yml (Serverless Framework) functions: getUser: handler: src/users.getUser events: - httpApi: path: /users/{id} method: GET createUser: handler: src/users.createUser events: - httpApi: path: /users method: POST
Scheduled function (cron)
yamlfunctions: dailyReport: handler: src/reports.generate events: - schedule: cron(0 8 * * ? *) # every day at 8:00 UTC
S3 trigger
javascriptexport const handler = async (event) => { for (const record of event.Records) { const bucket = record.s3.bucket.name; const key = record.s3.object.key; console.log(`Processing s3://${bucket}/${key}`); await processUploadedFile(bucket, key); } };
Cold Starts
When a Lambda function has not been invoked recently, AWS needs to initialize a new execution environment. This initialization is called a cold start and adds latency β typically 100ms to 1s depending on runtime and function size.
After the first invocation, the environment is reused for subsequent requests (warm start β much faster).
Minimizing cold starts
javascript// Initialize heavy resources outside the handler // They are reused across warm invocations import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; const dynamodb = new DynamoDBClient({ region: "eu-west-1" }); const secretCache = new Map(); export const handler = async (event) => { // dynamodb client is already initialized -- reused const result = await dynamodb.send(...); return result; };
Additional strategies:
- Keep functions small β less code to initialize
- Use lighter runtimes β Node.js and Python cold start faster than Java or .NET
- Use Provisioned Concurrency to keep environments warm (costs more)
- Use Lambda SnapStart (Java) to snapshot the initialized environment
Environment Variables and Secrets
yamlfunctions: api: handler: src/api.handler environment: NODE_ENV: production DB_HOST: ${ssm:/myapp/db/host} DB_NAME: myapp_prod
For sensitive values, fetch from AWS Secrets Manager at runtime rather than storing in environment variables:
javascriptimport { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; const sm = new SecretsManagerClient({ region: "eu-west-1" }); let cachedSecret; async function getDbPassword() { if (cachedSecret) return cachedSecret; const response = await sm.send(new GetSecretValueCommand({ SecretId: "myapp/db/password", })); cachedSecret = response.SecretString; return cachedSecret; }
Caching the secret in the module scope avoids fetching it on every invocation.
Lambda Layers
Layers are ZIP archives that can be shared across multiple functions β great for common dependencies or shared utilities:
yamllayers: commonDeps: path: layer compatibleRuntimes: - nodejs20.x functions: api: handler: src/api.handler layers: - !Ref CommonDepsLambdaLayer
Layers are mounted at /opt in the execution environment. Use them to:
- Share large libraries (e.g., Puppeteer, ffmpeg binaries) across functions
- Reduce deployment package size
- Update shared code without redeploying every function
Error Handling and Retries
Lambda retries failed invocations differently depending on the trigger:
- Synchronous invocations (API Gateway) β no automatic retry; error returned to caller
- Asynchronous invocations (S3, SNS) β retries up to 2 times with delays
- Queue-based (SQS) β retries until message expires or hits DLQ
Always configure a Dead Letter Queue (DLQ) for async and queue-based invocations:
yamlfunctions: processOrder: handler: src/orders.process events: - sqs: arn: !GetAtt OrdersQueue.Arn batchSize: 10 destinations: onFailure: arn:aws:sqs:eu-west-1:123456789:OrdersDLQ
javascriptexport const handler = async (event) => { const results = await Promise.allSettled( event.Records.map(record => processRecord(record)) ); // Report partial batch failures (SQS) const failures = results .map((r, i) => r.status === "rejected" ? { itemIdentifier: event.Records[i].messageId } : null) .filter(Boolean); return { batchItemFailures: failures }; // only failed messages return to queue };
IAM Permissions
Lambda functions need an IAM role with permissions for every AWS service they touch:
yamlprovider: iam: role: statements: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem Resource: !GetAtt UsersTable.Arn - Effect: Allow Action: - s3:GetObject Resource: arn:aws:s3:::my-bucket/* - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: arn:aws:secretsmanager:eu-west-1:*:secret:myapp/*
Always follow the principle of least privilege β grant only the exact permissions needed.
When to Use Serverless
Good fit:
- Event-driven workloads (file processing, message handling, webhooks)
- APIs with variable or unpredictable traffic
- Scheduled jobs and background tasks
- Microservices with low to moderate request rates
- Prototypes and MVPs (low upfront cost)
Poor fit:
- Long-running processes (Lambda max timeout is 15 minutes)
- Workloads requiring persistent connections (WebSockets need special handling)
- Very high throughput, low-latency APIs (cold starts add unpredictability)
- Applications needing more than 10GB RAM or 6 vCPUs
- Workflows requiring fine-grained OS or runtime control
Practice AWS on Froquiz
Cloud and serverless architecture are tested in backend and DevOps interviews. Explore our AWS and infrastructure quizzes on Froquiz across all difficulty levels.
Summary
- Lambda runs code in response to events β no server management
- Triggers: API Gateway, S3, SQS, EventBridge (cron), DynamoDB Streams, and more
- Cold starts add latency on first invocation β minimize with small packages and module-level initialization
- Store secrets in Secrets Manager, fetch and cache at function startup
- Layers share common dependencies and binaries across functions
- Always configure DLQs for async and queue-based functions
- IAM roles follow least privilege β grant only what each function needs
- Serverless shines for event-driven, variable-traffic workloads; not ideal for long-running or high-throughput steady-state work