Monitoring AWS Lambda: Metrics, Alarms, and Dashboards
When you deploy code to AWS Lambda, your responsibility doesn't end at deployment. You need visibility into how your functions perform, whether they're consuming resources efficiently, and when they fail. Monitoring is the foundation of operational excellence in serverless architectures. This tutorial walks you through everything you need to know about monitoring Lambda functions using Amazon CloudWatch metrics, alarms, and dashboards — from basic concepts to production-ready setups.
What Is Lambda Monitoring?
Lambda monitoring refers to the continuous collection, analysis, and visualization of data about your function invocations. AWS automatically sends key metrics to CloudWatch for every Lambda function, including invocation count, duration, error rate, and throttle events. Beyond these built-in metrics, you can emit custom metrics from within your function code to track business-specific events, cold starts, or downstream service latencies. The monitoring ecosystem includes three core pillars:
- Metrics — time-series data points that represent some aspect of your function's behavior
- Alarms — rules that trigger notifications or automated actions when metrics breach defined thresholds
- Dashboards — visual canvases that aggregate multiple metrics into a single, glanceable view
Why Monitoring Matters for Lambda
Serverless architectures abstract away infrastructure management, but they also obscure what's happening under the hood. Without monitoring, you're flying blind. Here's why investing in Lambda monitoring pays dividends:
- Cost optimization — identify functions with excessive duration or memory over-provisioning, then right-size them to reduce your bill
- Performance tuning — detect cold start patterns and high-latency outliers that degrade user experience
- Reliability engineering — catch error rate spikes before they become full-blown outages
- Throttle awareness — know when your concurrency limits are being hit so you can adjust reserved capacity
- Debugging — correlate invocations with downstream failures, timeouts, or resource exhaustion
- SLA compliance — prove to stakeholders that your function meets latency and availability targets
Built-In CloudWatch Metrics
Every Lambda function automatically publishes a set of core metrics to CloudWatch at no additional cost. These metrics are pushed at one-minute granularity and retained based on your CloudWatch retention settings. Understanding each metric is the first step toward effective monitoring.
Here are the essential built-in metrics and what they tell you:
Invocations— total number of times your function was invoked, including both successful and failed executions. Use this to understand traffic patterns.Errors— the count of failed invocations due to unhandled exceptions, timeouts, or crashes. This excludes retried invocations that eventually succeed.Duration— the wall-clock time your function code took to execute, measured in milliseconds. Combine with Invocations to estimate cost.Throttles— invocations rejected because you exceeded your account's concurrent execution limit. A non-zero throttle count demands immediate attention.ConcurrentExecutions— the number of invocations running simultaneously at any given moment. Track this to stay within regional limits.ProvisionedConcurrencyUtilization— for functions using provisioned concurrency, this shows how much of your allocation is being used.IteratorAge— for stream-based invocations (Kinesis, DynamoDB Streams), this measures the age of the oldest record still being processed.
These metrics are available in the CloudWatch console under Metrics → Lambda → By Function Name. You can also query them programmatically using the AWS CLI or SDKs.
Retrieving Metrics with the AWS CLI
Before building dashboards and alarms, it helps to fetch raw metric data directly. The following examples use the aws cloudwatch get-metric-statistics command. Replace FunctionName with your actual function name.
Get the average duration over the last hour for a specific function:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value=my-function \
--statistics Average \
--start-time "$(date -u -d '1 hour ago' '+%Y-%m-%dT%H:%M:%SZ')" \
--end-time "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--period 300
Retrieve the sum of errors over the past 24 hours at 1-hour granularity:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Errors \
--dimensions Name=FunctionName,Value=my-function \
--statistics Sum \
--start-time "$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ')" \
--end-time "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--period 3600
Check throttle counts — a critical indicator of concurrency exhaustion:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Throttles \
--dimensions Name=FunctionName,Value=my-function \
--statistics Sum \
--start-time "$(date -u -d '6 hours ago' '+%Y-%m-%dT%H:%M:%SZ')" \
--end-time "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--period 3600
Custom Metrics: Extending Visibility
Built-in metrics cover the Lambda execution lifecycle, but they don't reveal what your code actually does. Custom metrics fill this gap. You can emit any numeric value from within your function — for example, the number of records processed, the latency of an external API call, or a count of validation failures. These custom metrics appear in your own CloudWatch namespace and behave just like built-in metrics: you can graph them, alarm on them, and include them in dashboards.
To publish custom metrics, use the cloudwatch:PutMetricData API. The simplest way is via the AWS SDK bundled in the Lambda runtime, or by using the embedded metrics format (EMF), which we'll cover shortly.
Here's a Node.js function that records the response time of an external HTTP request as a custom metric:
const { CloudWatchClient, PutMetricDataCommand } = require("@aws-sdk/client-cloudwatch");
const https = require("https");
const cloudwatch = new CloudWatchClient({ region: "us-east-1" });
async function timedRequest(url) {
const start = Date.now();
return new Promise((resolve, reject) => {
https.get(url, (res) => {
const latency = Date.now() - start;
resolve({ statusCode: res.statusCode, latency });
}).on('error', reject);
});
}
exports.handler = async (event) => {
const result = await timedRequest("https://api.example.com/data");
// Emit a custom metric for the external API latency
const command = new PutMetricDataCommand({
Namespace: "MyApplication/ExternalAPIs",
MetricData: [
{
MetricName: "Latency",
Value: result.latency,
Unit: "Milliseconds",
Dimensions: [{ Name: "Endpoint", Value: "api.example.com" }],
Timestamp: new Date()
}
]
});
await cloudwatch.send(command);
return { statusCode: 200, body: JSON.stringify(result) };
};
And here's the equivalent in Python using boto3:
import boto3
import time
import urllib.request
cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')
def lambda_handler(event, context):
start = time.time()
with urllib.request.urlopen('https://api.example.com/data') as response:
body = response.read()
latency_ms = (time.time() - start) * 1000
cloudwatch.put_metric_data(
Namespace='MyApplication/ExternalAPIs',
MetricData=[{
'MetricName': 'Latency',
'Value': latency_ms,
'Unit': 'Milliseconds',
'Dimensions': [{'Name': 'Endpoint', 'Value': 'api.example.com'}],
'Timestamp': time.time()
}]
)
return {'statusCode': 200, 'body': body.decode()}
Embedded Metrics Format (EMF) — The Modern Approach
Manually calling PutMetricData inside your handler adds latency and cost (each call incurs a CloudWatch API charge). The Embedded Metrics Format solves both problems. With EMF, you write a structured JSON log to stdout, and Lambda automatically extracts metric definitions from it. CloudWatch parses the log, creates the metrics, and charges nothing extra — you only pay for the log ingestion.
EMF logs must include specific fields: _aws with Timestamp and CloudWatchMetrics containing a Namespace, Dimensions, and Metrics array. The AWS Lambda Powertools libraries for Python, Java, and TypeScript make this trivial.
Install the Python powertools layer or package, then emit metrics like this:
from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricUnit
import json
metrics = Metrics(namespace="MyApplication", service="orderProcessor")
@metrics.log_metrics # ensures metrics are flushed at the end
def lambda_handler(event, context):
order_count = len(event.get('orders', []))
metrics.add_metric(name="OrdersReceived", unit=MetricUnit.Count, value=order_count)
for order in event.get('orders', []):
# Process each order...
if order.get('status') == 'failed':
metrics.add_metric(name="FailedOrders", unit=MetricUnit.Count, value=1)
return {"statusCode": 200}
# Metrics are automatically serialized and logged on return
For TypeScript/Node.js, use the powertools package similarly:
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
const metrics = new Metrics({ namespace: 'MyApplication', serviceName: 'orderProcessor' });
export const handler = async (event: any): Promise => {
const orderCount = event.orders?.length || 0;
metrics.addMetric('OrdersReceived', MetricUnit.Count, orderCount);
for (const order of event.orders || []) {
if (order.status === 'failed') {
metrics.addMetric('FailedOrders', MetricUnit.Count, 1);
}
}
// Powertools automatically flushes metrics when the handler returns
return { statusCode: 200 };
};
The key advantage: no blocking API calls, no additional latency, and zero-cost custom metrics beyond standard log pricing.
Setting Up CloudWatch Alarms
Metrics alone provide visibility, but alarms turn that visibility into action. A CloudWatch alarm watches a metric over a time period and performs one or more actions when the metric crosses a threshold. For Lambda, the most critical alarms to configure are error rate spikes, elevated duration, throttling, and concurrency exhaustion.
Alarms can send notifications via Amazon SNS (email, SMS, Slack via Lambda relay), trigger auto-scaling actions, or invoke a Lambda function for self-healing workflows.
Create an alarm that triggers when your function's error rate exceeds 5% over a 5-minute evaluation window:
aws cloudwatch put-metric-alarm \
--alarm-name "my-function-high-error-rate" \
--alarm-description "Alert when error rate exceeds 5% over 5 minutes" \
--namespace AWS/Lambda \
--metric-name Errors \
--dimensions Name=FunctionName,Value=my-function \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 5 \
--comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts
For a more sophisticated alarm that uses a math expression to calculate error rate as a percentage of invocations, you'll need the put-metric-alarm with the --metrics array syntax:
aws cloudwatch put-metric-alarm \
--alarm-name "my-function-error-percentage" \
--alarm-description "Error rate percentage alarm" \
--metrics '[{
"Id": "errors",
"MetricStat": {
"Metric": {
"Namespace": "AWS/Lambda",
"MetricName": "Errors",
"Dimensions": [{"Name": "FunctionName", "Value": "my-function"}]
},
"Stat": "Sum",
"Period": 300
},
"ReturnData": false
}, {
"Id": "invocations",
"MetricStat": {
"Metric": {
"Namespace": "AWS/Lambda",
"MetricName": "Invocations",
"Dimensions": [{"Name": "FunctionName", "Value": "my-function"}]
},
"Stat": "Sum",
"Period": 300
},
"ReturnData": false
}, {
"Id": "errorRate",
"Expression": "(errors / invocations) * 100",
"Label": "ErrorRatePercentage",
"ReturnData": true
}]' \
--evaluation-periods 1 \
--threshold 5 \
--comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts
Here's a critical alarm for detecting throttles — when this fires, you're losing invocations:
aws cloudwatch put-metric-alarm \
--alarm-name "my-function-throttling-detected" \
--alarm-description "Any throttling in the last 5 minutes" \
--namespace AWS/Lambda \
--metric-name Throttles \
--dimensions Name=FunctionName,Value=my-function \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts
For duration anomalies, alarm when the p99 (or average) duration exceeds a threshold. This helps catch performance regressions after deployments:
aws cloudwatch put-metric-alarm \
--alarm-name "my-function-high-duration" \
--alarm-description "p99 duration exceeds 3000ms" \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value=my-function \
--extended-statistic p99 \
--period 300 \
--evaluation-periods 3 \
--threshold 3000 \
--comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts
Building CloudWatch Dashboards
Dashboards give you a unified view of your Lambda estate. Instead of clicking through individual metric graphs in the console, you build a custom dashboard that shows everything at once: invocation volumes, error rates, duration percentiles, throttle counts, and custom business metrics — all on a single screen.
You can create dashboards via the AWS Console, but for reproducibility, use the CLI or Infrastructure as Code (CloudFormation, CDK, Terraform). The following CLI example creates a dashboard widget that displays multiple Lambda metrics side by side.
First, create a new dashboard:
aws cloudwatch create-dashboard \
--dashboard-name "Lambda-Monitoring-Overview"
Then add widgets by specifying a JSON body. Here's a comprehensive dashboard configuration that shows invocations, errors, duration, and throttles for two functions:
aws cloudwatch put-dashboard \
--dashboard-name "Lambda-Monitoring-Overview" \
--dashboard-body '{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"view": "timeSeries",
"stacked": false,
"metrics": [
["AWS/Lambda", "Invocations", {"FunctionName": "orderProcessor", "stat": "Sum", "period": 60}],
["AWS/Lambda", "Invocations", {"FunctionName": "paymentHandler", "stat": "Sum", "period": 60}]
],
"region": "us-east-1",
"title": "Invocations (1-min)",
"yAxis": {"left": {"label": "Count", "min": 0}}
}
},
{
"type": "metric",
"x": 12,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"view": "timeSeries",
"stacked": false,
"metrics": [
["AWS/Lambda", "Errors", {"FunctionName": "orderProcessor", "stat": "Sum", "period": 60}],
["AWS/Lambda", "Errors", {"FunctionName": "paymentHandler", "stat": "Sum", "period": 60}]
],
"region": "us-east-1",
"title": "Errors (1-min)",
"yAxis": {"left": {"label": "Count", "min": 0}}
}
},
{
"type": "metric",
"x": 0,
"y": 6,
"width": 12,
"height": 6,
"properties": {
"view": "timeSeries",
"stacked": false,
"metrics": [
["AWS/Lambda", "Duration", {"FunctionName": "orderProcessor", "stat": "p99", "period": 300}],
["AWS/Lambda", "Duration", {"FunctionName": "orderProcessor", "stat": "p50", "period": 300}],
["AWS/Lambda", "Duration", {"FunctionName": "orderProcessor", "stat": "Average", "period": 300}]
],
"region": "us-east-1",
"title": "orderProcessor Duration Percentiles (5-min)",
"yAxis": {"left": {"label": "Milliseconds", "min": 0}}
}
},
{
"type": "metric",
"x": 12,
"y": 6,
"width": 12,
"height": 6,
"properties": {
"view": "timeSeries",
"stacked": false,
"metrics": [
["AWS/Lambda", "Throttles", {"FunctionName": "orderProcessor", "stat": "Sum", "period": 300}],
["AWS/Lambda", "Throttles", {"FunctionName": "paymentHandler", "stat": "Sum", "period": 300}]
],
"region": "us-east-1",
"title": "Throttles (5-min)",
"yAxis": {"left": {"label": "Count", "min": 0}}
}
},
{
"type": "metric",
"x": 0,
"y": 12,
"width": 24,
"height": 6,
"properties": {
"view": "timeSeries",
"stacked": false,
"metrics": [
["MyApplication/ExternalAPIs", "Latency", {"Endpoint": "api.example.com", "stat": "Average", "period": 300}]
],
"region": "us-east-1",
"title": "Custom: External API Latency (5-min avg)",
"yAxis": {"left": {"label": "Milliseconds", "min": 0}}
}
}
]
}'
This creates a 4-row dashboard with invocation counts, error counts, duration percentiles, throttle monitoring, and custom external API latency — all in one glanceable view.
Using Infrastructure as Code for Dashboards
For production systems, define dashboards alongside your Lambda functions using AWS CloudFormation or CDK. Here's a CDK example in TypeScript that creates a dashboard with an error rate alarm and a duration widget:
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class LambdaMonitoringStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Reference an existing Lambda function (or create one)
const myFunction = lambda.Function.fromFunctionArn(
this,
'ImportedFunction',
'arn:aws:lambda:us-east-1:123456789012:function:orderProcessor'
);
// Create a dashboard
const dashboard = new cloudwatch.Dashboard(this, 'LambdaDashboard', {
dashboardName: 'OrderProcessor-Monitoring',
});
// Add invocation count widget
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Invocations',
left: [myFunction.metricInvocations({ period: cdk.Duration.minutes(1) })],
width: 12,
height: 6,
})
);
// Add error count widget
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Errors',
left: [myFunction.metricErrors({ period: cdk.Duration.minutes(1) })],
width: 12,
height: 6,
})
);
// Add duration widget with multiple statistics
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Duration (ms)',
left: [
myFunction.metricDuration({ statistic: 'p99', period: cdk.Duration.minutes(5) }),
myFunction.metricDuration({ statistic: 'p50', period: cdk.Duration.minutes(5) }),
myFunction.metricDuration({ statistic: 'Average', period: cdk.Duration.minutes(5) }),
],
width: 24,
height: 6,
})
);
// Create an alarm on error rate and add to dashboard
const errorAlarm = new cloudwatch.Alarm(this, 'HighErrorRate', {
metric: myFunction.metricErrors({ period: cdk.Duration.minutes(5) }),
threshold: 5,
evaluationPeriods: 1,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
alarmName: 'orderProcessor-high-error-rate',
});
dashboard.addWidgets(
new cloudwatch.AlarmWidget({
title: 'Error Rate Alarm',
alarm: errorAlarm,
width: 12,
height: 6,
})
);
}
}
Best Practices for Lambda Monitoring
After building monitoring for dozens of production serverless systems, certain patterns consistently prove their worth. Here's a distilled set of best practices to guide your monitoring strategy:
- Alarm on error rate, not raw error count — a spike from 1 to 5 errors is alarming on a function that normally receives 10 invocations per minute, but trivial on one receiving 10,000. Use metric math expressions to calculate percentages.
- Monitor duration at multiple percentiles — average duration hides tail latency. Track p50 (median), p90, and p99 simultaneously. A p99 spike often signals cold starts or resource contention that the average won't reveal.
- Set throttle alarms to fire on any occurrence — throttling means lost work. Even a single throttle event in a 5-minute window deserves an alert.
- Use EMF for all custom metrics — avoid the latency and cost of synchronous
PutMetricDatacalls. EMF gives you free custom metrics with zero performance impact. - Include function version/alias as a dimension — when deploying new versions, tag metrics with the alias (e.g.,
prod,canary) so you can isolate the impact of a deployment. - Build dashboards per service domain — instead of one monolithic dashboard, create focused dashboards for each business domain (orders, payments, notifications) so on-call engineers see relevant data immediately.
- Define dashboards in Infrastructure as Code — console-built dashboards are not reproducible. Use CDK, CloudFormation, or Terraform so dashboards are versioned alongside your function code.
- Set composite alarms for complex conditions — combine multiple alarms into a composite alarm that triggers only when both "high error rate" AND "high invocation count" are true, reducing noise from transient low-traffic anomalies.
- Monitor IteratorAge for stream functions — if you use Kinesis or DynamoDB Streams, a growing IteratorAge means you're falling behind. Alarm on it aggressively.
- Test your alarms — periodically simulate failure conditions (throw exceptions in a test function) to verify that alarms fire and notifications reach the intended recipients.
- Set anomaly detection on key metrics — CloudWatch anomaly detection learns your function's baseline patterns and alerts on deviations without requiring manual threshold tuning. This is especially useful for invocation counts with seasonal traffic patterns.
Conclusion
Monitoring Lambda functions effectively means moving beyond the default console graphs and building a deliberate observability strategy. Start by understanding the built-in metrics CloudWatch provides for free, then layer on custom metrics via EMF to capture business-specific signals. Configure alarms that alert on error rates, throttling, and duration anomalies — and route those alerts to the right people. Finally, consolidate everything into purpose-built dashboards that give you instant situational awareness. When done well, Lambda monitoring transforms your serverless architecture from a black box into a transparent, predictable, and resilient system. The investment pays for itself the first time an alarm catches a production issue before your users do.