← Back to DevBytes

Monitoring Lambda: Metrics, Alarms, and Dashboards

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:

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:

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:

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles