Lambda fundamentals
AWS Lambda lets you run code without managing servers—you upload a function, and AWS handles the infrastructure. I've built dozens of production systems relying on Lambda for everything from API endpoints to asynchronous workers, and it remains one of the most practical ways to move fast without operational overhead. Understanding how Lambda works, its constraints, and its cost model is essential before you build anything serious on it.
Event-driven execution
Lambda functions don't run continuously. They sit dormant until an event triggers them. This is the core idea: you define what should wake up your function, and AWS invokes it only when that event occurs. In my experience, this model forces you to think clearly about your system's flow—what needs to happen, when, and why.
API Gateway is the most common trigger I use. When a client makes an HTTP request to an API Gateway endpoint, it directly invokes your Lambda function. The request payload becomes the event object your function receives.
// Node.js Lambda handler
export const handler = async (event) => {
console.log('Received:', event);
const httpMethod = event.httpMethod;
const body = JSON.parse(event.body);
// Process the request
return {
statusCode: 200,
body: JSON.stringify({ message: 'Success' })
};
};SQS queues trigger Lambda functions when messages arrive. This is invaluable for decoupling services. I'll push work onto a queue from an API handler, and a separate Lambda function processes messages at its own pace. If the Lambda fails, the message stays in the queue and gets retried automatically.
S3 events trigger Lambda when objects are uploaded, deleted, or modified. I've used this for image resizing workflows: upload an image, S3 triggers a Lambda that generates thumbnails, and stores them back in S3. No background job server needed.
CloudWatch Events (EventBridge) lets you schedule Lambda functions using cron expressions. Run daily reports, cleanup jobs, or sync data on a schedule. This replaces traditional cron servers entirely.
DynamoDB Streams trigger Lambda when records change. This is powerful for maintaining denormalized caches or syncing data to Elasticsearch when your primary database updates.
Pay-per-use pricing model
Lambda's pricing is straightforward: you pay for the number of requests and the duration your function runs. Specifically, AWS bills you per 100 milliseconds of execution time, multiplied by the amount of memory you allocate.
The critical insight is that idle cost is zero. Unlike an EC2 instance running 24/7, a Lambda function sitting unused costs you nothing. This makes Lambda economical for workloads with irregular traffic patterns. A function that runs for 1 second per day across 1 million days costs far less than maintaining an always-on server.
The pricing formula is simple: GB-seconds consumed × $0.0000166667 (US East 1 rates). If you allocate 256 MB (0.25 GB) and your function runs for 1 second, that's 0.25 GB-seconds. At scale, say 1 million executions per month, each running 2 seconds, you're looking at roughly $33 for that month.
I usually allocate more memory than the minimum needed, even though it increases per-millisecond cost. Here's why: Lambda allocates CPU proportionally to memory. More memory means faster execution, which often saves money overall. A function with 128 MB might take 5 seconds, while the same function with 1024 MB takes 1 second. The 1024 MB version might cost less because it finishes faster.
There's also a free tier: 1 million requests and 400,000 GB-seconds monthly. Useful for small projects or learning, but production systems will exceed it quickly.
Resource limits and constraints
Lambda functions have hard limits you need to understand before designing your system. The most important one: maximum execution time is 15 minutes (900 seconds). If your function needs longer, Lambda terminates it. This doesn't mean Lambda is unsuitable for long-running work—it means you architect differently, using queues and smaller functions that complete within the time window.
Memory allocation ranges from 128 MB to 10 GB. Choose based on your workload. I typically start at 512 MB for API handlers and 1-2 GB for compute-heavy work. Less memory is cheaper, but if your function times out due to hitting memory limits or swapping, increasing memory often pays for itself in faster execution.
Ephemeral storage (the /tmp directory) is 512 MB by default, expandable to 10 GB. This is temporary disk space your function can use during execution. Useful for downloading large files, processing them, and uploading results. Once the execution ends, the storage is wiped.
Concurrency limits apply per function. By default, your account can run up to 1000 concurrent Lambda executions. This means if you have 2 functions, they share that quota. Reserved concurrency lets you dedicate concurrency to specific functions, useful for production systems where you want predictable performance for critical services.
Environment variables are limited to 4 KB, so keep configuration lightweight. Use Secrets Manager or Parameter Store for larger or sensitive data.
Payload sizes are limited to 6 MB for synchronous invocations and 256 KB for asynchronous ones. For larger payloads, write data to S3 and pass the S3 key through the event.
Stateless design and external storage
Lambda functions are stateless by design. Each invocation runs in isolation. Data stored in memory during one execution disappears when that execution ends. This is actually a feature—it forces you to design systems properly—but it trips up developers coming from long-running servers.
Never rely on local variables persisting across invocations. If your function needs to remember something, store it externally.
DynamoDB is my go-to for state. It's serverless (no provisioning), scales automatically, and integrates seamlessly with Lambda. Session data, user preferences, temporary locks—all go in DynamoDB.
// Node.js + DynamoDB
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
const dynamodb = new DynamoDBClient({ region: 'us-east-1' });
export const handler = async (event) => {
const userId = event.pathParameters.userId;
const command = new PutItemCommand({
TableName: 'UserSessions',
Item: {
userId: { S: userId },
lastActive: { N: String(Date.now()) },
ttl: { N: String(Math.floor(Date.now() / 1000) + 3600) } // Expires in 1 hour
}
});
await dynamodb.send(command);
return { statusCode: 200, body: 'Session recorded' };
};Redis (via ElastiCache or an external service like Upstash) is excellent for caching and fast lookups. Queries on frequently accessed data hit Redis instead of the database, reducing latency and cost.
S3 stores large objects and files. If your Lambda processes a file, reads data, and generates output, store everything in S3. Use presigned URLs to give clients direct access without routing through your Lambda.
CloudWatch Logs captures everything your function prints. Structured logging (JSON format) makes searching and analyzing logs much easier when debugging production issues.
Common mistakes
Creating database connections inside the handler. Every invocation would create a new connection, overwhelming your database. Move connection creation outside the handler into the global scope. AWS reuses the same execution environment for subsequent invocations, so the connection persists.
// Wrong: connection per invocation
export const handler = async (event) => {
const conn = await db.connect(); // Creates connection EVERY time
// ...
};
// Right: connection reused across invocations
const conn = db.createConnection(); // Reused by same environment
export const handler = async (event) => {
// Use conn
// ...
};Assuming cold starts don't matter. Cold starts—when Lambda needs to initialize a new execution environment—add 100ms to 1 second of latency. For real-time APIs, this is noticeable. For asynchronous jobs, it's irrelevant. Design accordingly, or use Provisioned Concurrency to keep environments warm (though it adds cost).
Storing secrets in environment variables as plain text. Use AWS Secrets Manager or Parameter Store. Your code retrieves the secret at runtime, never storing it in code or plain environment variables.
Not setting function timeouts appropriately. The default is 3 seconds. If your function legitimately needs 30 seconds, set it. If you don't increase the timeout and the function takes longer, it gets killed mid-execution.
Ignoring the 15-minute limit. If you need to process a task in more than 15 minutes, Lambda isn't the tool. Use Step Functions to orchestrate multiple Lambda executions, or run an ECS task.
Key takeaways
- Lambda functions are event-driven—triggered by API calls, queues, schedules, or storage events. Design your system around these triggers.
- You pay only for executions and duration. Idle functions cost nothing, making Lambda economical for irregular workloads.
- Understand the hard limits: 15-minute max runtime, 10 GB max memory, 512 MB default ephemeral storage. Design within these constraints.
- Functions are stateless. Store any data that needs to persist in DynamoDB, Redis, S3, or other external services.
- Move connection initialization and expensive setup outside the handler to take advantage of execution environment reuse.
- Monitor cold start latency for synchronous APIs; for async work, it's usually negligible.
- Lambda scales automatically, but be aware of concurrency limits and reserved concurrency for critical services.






Comments
Loading comments…