---
title: "AWS Lambda and Serverless Function Patterns"
description: "Handler design, cold start optimization, layers, event sources, VPC connectivity, and monitoring patterns for AWS Lambda functions."
url: https://agent-zone.ai/knowledge/serverless/lambda-functions-patterns/
section: knowledge
date: 2026-02-22
categories: ["serverless"]
tags: ["aws-lambda","serverless","cold-start","api-gateway","sqs","eventbridge","cloudwatch","layers"]
skills: ["lambda-deployment","serverless-architecture","event-driven-design","cold-start-optimization"]
tools: ["aws-lambda","aws-api-gateway","aws-sqs","aws-s3","aws-eventbridge","aws-cloudwatch","aws-sam","serverless-framework"]
levels: ["intermediate"]
word_count: 1269
formats:
  json: https://agent-zone.ai/knowledge/serverless/lambda-functions-patterns/index.json
  html: https://agent-zone.ai/knowledge/serverless/lambda-functions-patterns/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=AWS+Lambda+and+Serverless+Function+Patterns
---


# AWS Lambda and Serverless Function Patterns

Lambda runs your code without you provisioning or managing servers. You upload a function, configure a trigger, and AWS handles scaling, patching, and availability. The execution model is simple: an event arrives, Lambda invokes your handler, your handler returns a response. Everything in between -- concurrency, retries, scaling from zero to thousands of instances -- is managed for you.

That simplicity hides real complexity. Cold starts, timeout limits, memory-to-CPU coupling, VPC attachment latency, and event source mapping behavior all require deliberate design. This article covers the patterns that matter in practice.

## Handler Design

A Lambda handler receives an event and a context object. The event shape depends on the trigger. The context provides metadata like the remaining execution time, request ID, and log group.

**Node.js handler:**

```javascript
export const handler = async (event, context) => {
  // event shape depends on the trigger (API Gateway, SQS, S3, etc.)
  const { httpMethod, path, body } = event;

  try {
    const result = await processRequest(JSON.parse(body));
    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(result),
    };
  } catch (err) {
    console.error("Handler error:", JSON.stringify(err));
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Internal server error" }),
    };
  }
};
```

**Python handler:**

```python
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    logger.info(f"Request ID: {context.aws_request_id}")
    logger.info(f"Remaining time: {context.get_remaining_time_in_millis()}ms")

    try:
        body = json.loads(event.get("body", "{}"))
        result = process_request(body)
        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps(result)
        }
    except Exception as e:
        logger.exception("Handler failed")
        return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
```

**Key principles:**

- Initialize SDK clients, database connections, and configuration outside the handler function. Code at the module level runs once per container init and is reused across invocations.
- Keep handlers thin. Parse the event, call business logic, format the response. Testability comes from separating orchestration from logic.
- Always log the request ID from context for tracing.

```javascript
// Good: connections initialized outside the handler
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});

export const handler = async (event) => {
  // client is reused across warm invocations
  const result = await client.send(/* ... */);
  return { statusCode: 200, body: JSON.stringify(result) };
};
```

## Cold Start Optimization

A cold start occurs when Lambda creates a new execution environment: downloading your code, starting the runtime, running your init code, then invoking the handler. Subsequent invocations on the same container skip init entirely.

**What affects cold start duration:**

| Factor | Impact | Mitigation |
|---|---|---|
| Runtime | Java/C# are slowest (1-5s), Node.js/Python fastest (100-300ms) | Use Node.js or Python for latency-sensitive paths |
| Package size | Larger deployment packages take longer to download and extract | Minimize dependencies, use tree shaking, avoid bundling unused SDKs |
| VPC attachment | Adds 1-2 seconds for ENI creation | Use VPC only when required; Hyperplane has reduced this significantly |
| Memory allocation | More memory = proportionally more CPU = faster init | Increase memory even if you do not need the RAM, you need the CPU |
| Provisioned concurrency | Pre-warms N containers | Use for latency-critical functions |

**Provisioned concurrency** keeps a specified number of execution environments warm. The tradeoff is cost -- you pay for provisioned concurrency whether invocations arrive or not.

```bash
aws lambda put-provisioned-concurrency-config \
  --function-name my-api \
  --qualifier prod \
  --provisioned-concurrent-executions 10
```

**SnapStart** (Java only) takes a snapshot of the initialized execution environment after init and restores from it on cold start. This cuts Java cold starts from seconds to hundreds of milliseconds.

## Lambda Layers

Layers let you share code and dependencies across multiple functions without bundling them into each deployment package.

```bash
# Create a layer from a zip of dependencies
mkdir -p layer/nodejs
cd layer/nodejs && npm install pg redis
cd .. && zip -r my-deps-layer.zip nodejs/

aws lambda publish-layer-version \
  --layer-name shared-deps \
  --zip-file fileb://my-deps-layer.zip \
  --compatible-runtimes nodejs20.x
```

Attach the layer to a function:

```bash
aws lambda update-function-configuration \
  --function-name my-function \
  --layers arn:aws:lambda:us-east-1:123456789:layer:shared-deps:1
```

Layers are extracted to `/opt` at runtime. For Node.js, `/opt/nodejs/node_modules` is automatically on the module path. For Python, use `/opt/python`.

**When layers help:** shared utility code, large dependencies (numpy, pandas), custom runtimes. **When they hurt:** version coupling across functions, layer size limits (250 MB unzipped total across all layers), debugging complexity when the layer version diverges from what you tested locally.

## Event Sources

Lambda integrates with dozens of AWS services. The most common patterns:

**API Gateway (synchronous):**

```yaml
# SAM template
Events:
  ApiEvent:
    Type: Api
    Properties:
      Path: /users/{id}
      Method: GET
      RestApiId: !Ref MyApi
```

The function receives the full HTTP request and must return a response object. API Gateway waits for the response -- the caller is blocked until the function completes or times out (max 29 seconds for API Gateway).

**SQS (asynchronous, batched):**

```yaml
Events:
  SqsEvent:
    Type: SQS
    Properties:
      Queue: !GetAtt OrderQueue.Arn
      BatchSize: 10
      MaximumBatchingWindowInSeconds: 5
      FunctionResponseTypes:
        - ReportBatchItemFailures
```

Lambda polls SQS and invokes your function with a batch of messages. The critical setting is `ReportBatchItemFailures` -- without it, any single failure in a batch causes the entire batch to be retried. With it, your handler returns which specific messages failed, and only those are retried.

```javascript
export const handler = async (event) => {
  const failures = [];
  for (const record of event.Records) {
    try {
      await processMessage(JSON.parse(record.body));
    } catch (err) {
      failures.push({ itemIdentifier: record.messageId });
    }
  }
  return { batchItemFailures: failures };
};
```

**S3 (event notification):**

```yaml
Events:
  S3Upload:
    Type: S3
    Properties:
      Bucket: !Ref UploadBucket
      Events: s3:ObjectCreated:*
      Filter:
        S3Key:
          Rules:
            - Name: prefix
              Value: uploads/
```

**EventBridge (event bus):**

```yaml
Events:
  OrderCreated:
    Type: EventBridgeRule
    Properties:
      Pattern:
        source: ["com.myapp.orders"]
        detail-type: ["OrderCreated"]
```

EventBridge is the preferred pattern for decoupled event-driven architectures. It supports content-based filtering, multiple targets per rule, archive and replay, and schema discovery.

## Environment Variables and Configuration

```bash
aws lambda update-function-configuration \
  --function-name my-function \
  --environment "Variables={DB_HOST=mydb.cluster.us-east-1.rds.amazonaws.com,LOG_LEVEL=info}"
```

For secrets, do not put them in environment variables in plaintext. Use AWS Systems Manager Parameter Store or Secrets Manager, and fetch at init time:

```python
import boto3
import os

ssm = boto3.client("ssm")

# Runs once per cold start, cached across invocations
DB_PASSWORD = ssm.get_parameter(
    Name="/myapp/prod/db-password",
    WithDecryption=True
)["Parameter"]["Value"]
```

For frequently changing configuration, use the Lambda Extensions API with a caching layer rather than redeploying the function.

## VPC Connectivity

Lambda functions run in an AWS-managed VPC by default and have internet access but cannot reach resources in your VPC. To access RDS, ElastiCache, or other VPC resources, attach the function to your VPC subnets:

```yaml
VpcConfig:
  SecurityGroupIds:
    - sg-0abc123
  SubnetIds:
    - subnet-private-1a
    - subnet-private-1b
```

**Critical considerations:**

- Place Lambda in private subnets. It does not need a public IP. If the function needs internet access (calling external APIs), route through a NAT Gateway.
- Lambda uses Hyperplane ENIs (shared across functions in the same security group and subnet combination), so the old cold-start penalty for VPC is largely eliminated, but initial setup of the ENI pool for a new combination still takes a few seconds.
- Security groups must allow outbound traffic to your database or cache on the appropriate port.

## Monitoring with CloudWatch

Every Lambda invocation logs to CloudWatch Logs automatically. Key metrics to monitor:

```bash
# View recent invocation metrics
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Duration \
  --dimensions Name=FunctionName,Value=my-function \
  --start-time 2026-02-22T00:00:00Z \
  --end-time 2026-02-22T23:59:59Z \
  --period 300 \
  --statistics Average p99

# Key metrics to alarm on
# - Errors (invocation errors)
# - Throttles (concurrency limit hit)
# - Duration p99 (approaching timeout)
# - ConcurrentExecutions (approaching account/function limit)
# - IteratorAge (for stream-based sources -- how far behind you are)
```

**CloudWatch Embedded Metric Format** lets you emit custom metrics directly from log output without making API calls:

```javascript
import { createMetricsLogger, Unit } from "aws-embedded-metrics";

export const handler = async (event) => {
  const metrics = createMetricsLogger();
  metrics.setNamespace("MyApp");
  metrics.putMetric("OrderProcessed", 1, Unit.Count);
  metrics.putMetric("ProcessingTime", elapsed, Unit.Milliseconds);
  metrics.setDimensions({ Service: "OrderProcessor" });
  await metrics.flush();
};
```

Set alarms on the metrics that predict problems before they become outages: Duration approaching the configured timeout, Throttles above zero, and IteratorAge growing for stream consumers.

