← Back to DevBytes

Monitoring App Runner: Metrics, Alarms, and Dashboards

Monitoring AWS App Runner: Metrics, Alarms, and Dashboards

When you deploy a containerized web application on AWS App Runner, you get a fully managed service that handles scaling, load balancing, and infrastructure provisioning. But once your application is live, you need deep visibility into how it performs under real-world traffic. This tutorial walks you through the complete monitoring stack for App Runner — from built-in CloudWatch metrics to custom alarms and unified dashboards — with practical code examples you can deploy today.

What Is App Runner Monitoring?

Monitoring for App Runner is built on Amazon CloudWatch, which collects and stores performance data as time-series metrics. App Runner automatically publishes a set of metrics every minute without any configuration on your part. These metrics tell you how many requests your service receives, how long it takes to respond, what percentage of requests fail, and how much CPU and memory your instances consume. You can view these metrics in the CloudWatch console, retrieve them programmatically via the CLI or SDK, and build alarms that trigger automated actions when thresholds are breached.

Why Monitoring Matters for App Runner

Without monitoring, a production App Runner service is a black box. Here is why investing time in metrics and alarms pays off immediately:

Built-in App Runner Metrics

App Runner emits metrics to the AWS/AppRunner namespace. Every metric is scoped to a specific ServiceId and optionally to an AppRunnerInstanceId if you need instance-level granularity. Here are the key metrics you should instrument from day one:

Core Service Metrics

Retrieving Metrics with the AWS CLI

Let's pull the average request latency for a specific App Runner service over the last hour. Replace the service ARN with your own:

aws cloudwatch get-metric-statistics \
    --namespace AWS/AppRunner \
    --metric-name RequestLatency \
    --statistics Average \
    --period 300 \
    --start-time "$(date -u -d '1 hour ago' '+%Y-%m-%dT%H:%M:%SZ')" \
    --end-time "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
    --dimensions Name=ServiceId,Value=arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def \
    --output table

This returns a table of timestamps and average latency values at 5-minute intervals. The --period value must align with the metric's native resolution. For App Runner, metrics are published every 1 minute, so periods of 60 seconds or multiples thereof are valid.

Using the CloudWatch Metric Query (Metrics Insights)

CloudWatch Metrics Insights provides a powerful SQL-like query language. To graph the 99th percentile latency alongside error count:

SELECT SUM(RequestLatency.p99) AS p99_latency_ms, 
       SUM(ErrorCount) AS errors
FROM AWS/AppRunner
WHERE ServiceId = 'arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def'
GROUP BY ServiceId
ORDER BY MAX() DESC
LIMIT 20

Run this query interactively in the CloudWatch console under Metrics > Query or embed it in a dashboard widget for persistent visibility.

Setting Up CloudWatch Alarms

Alarms watch a single metric over a time window and trigger an action — typically an SNS notification — when the metric crosses a defined threshold. For App Runner, the most impactful alarms fall into three categories: availability, performance, and resource saturation.

Alarm: High Error Rate (Availability)

This alarm fires when your service returns more than a handful of errors per minute over a sustained period. A 5-minute evaluation period with 3 out of 5 datapoints breaching prevents flapping from transient spikes.

aws cloudwatch put-metric-alarm \
    --alarm-name "AppRunner-HighErrorRate" \
    --alarm-description "Triggers when error count exceeds 10 per minute for 5 minutes" \
    --namespace AWS/AppRunner \
    --metric-name ErrorCount \
    --statistic Sum \
    --period 300 \
    --evaluation-periods 3 \
    --threshold 10 \
    --comparison-operator GreaterThanThreshold \
    --dimensions Name=ServiceId,Value=arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def \
    --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-team-notifications \
    --treat-missing-data missing

Key parameters explained:

Alarm: High Response Latency (Performance)

Monitor the 99th percentile latency to catch tail-latency problems that affect a subset of users but ruin the overall experience. App Runner publishes RequestLatency.p99 as a separate metric statistic.

aws cloudwatch put-metric-alarm \
    --alarm-name "AppRunner-P99-Latency-High" \
    --alarm-description "Triggers when p99 latency exceeds 2000ms for 5 minutes" \
    --namespace AWS/AppRunner \
    --metric-name RequestLatency \
    --extended-statistic p99 \
    --period 300 \
    --evaluation-periods 3 \
    --threshold 2000 \
    --comparison-operator GreaterThanThreshold \
    --dimensions Name=ServiceId,Value=arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def \
    --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-team-notifications \
    --treat-missing-data notBreaching

Notice the use of --extended-statistic p99 instead of --statistic. Extended statistics like p90, p95, p99 require this flag. Also note treat-missing-data notBreaching — if the service is idle (no requests), we do not want a false alarm; missing data is considered healthy.

Alarm: High CPU Utilization (Resource Saturation)

Sustained high CPU can indicate that instances are undersized or that a background task is consuming cycles. This alarm uses the Average statistic across instances.

aws cloudwatch put-metric-alarm \
    --alarm-name "AppRunner-HighCPU" \
    --alarm-description "Triggers when average CPU exceeds 85% for 10 minutes" \
    --namespace AWS/AppRunner \
    --metric-name CPUUtilization \
    --statistic Average \
    --period 300 \
    --evaluation-periods 2 \
    --threshold 85 \
    --comparison-operator GreaterThanThreshold \
    --dimensions Name=ServiceId,Value=arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def \
    --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-team-notifications \
    --treat-missing-data missing

Composite Alarms for Noise Reduction

Instead of paging on-call for every individual alarm, combine related alarms into a composite. A composite alarm fires only when multiple underlying alarms are in ALARM state simultaneously, reducing noise from transient, isolated issues.

aws cloudwatch put-composite-alarm \
    --alarm-name "AppRunner-ServiceDegraded" \
    --alarm-description "Composite: fires when both error rate and latency alarms breach" \
    --alarm-rule "ALARM(\"AppRunner-HighErrorRate\") AND ALARM(\"AppRunner-P99-Latency-High\")" \
    --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-team-critical \
    --treat-missing-data missing

This composite alarm only escalates when users are experiencing both errors and high latency — a true service degradation — filtering out isolated metric spikes that resolve on their own.

Building CloudWatch Dashboards

Dashboards give you a single pane of glass across all your App Runner services. You can build them interactively in the console, but for repeatability across environments, define your dashboard as infrastructure-as-code using the CLI or CloudFormation. Below is a complete CLI example that creates a production-grade dashboard.

Creating a Dashboard with the CLI

aws cloudwatch put-dashboard \
    --dashboard-name "AppRunner-Production-Overview" \
    --dashboard-body '{
  "widgets": [
    {
      "type": "metric",
      "x": 0,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/AppRunner", "RequestCount", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "Sum", "label": "Requests" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "Request Volume",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 12,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/AppRunner", "RequestLatency", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "p50", "label": "p50" } ],
          [ "AWS/AppRunner", "RequestLatency", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "p90", "label": "p90" } ],
          [ "AWS/AppRunner", "RequestLatency", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "p99", "label": "p99" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "Request Latency (ms)",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 0,
      "y": 6,
      "width": 8,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/AppRunner", "ErrorCount", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "Sum", "label": "Errors" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "HTTP Errors (4xx + 5xx)",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 8,
      "y": 6,
      "width": 8,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/AppRunner", "CPUUtilization", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "Average", "label": "CPU %" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "CPU Utilization",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 16,
      "y": 6,
      "width": 8,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/AppRunner", "MemoryUtilization", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "Average", "label": "Memory %" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "Memory Utilization",
        "period": 300
      }
    },
    {
      "type": "metric",
      "x": 0,
      "y": 12,
      "width": 24,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/AppRunner", "InstanceCount", "ServiceId", "arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def", { "stat": "Maximum", "label": "Active Instances" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "Instance Count",
        "period": 300
      }
    },
    {
      "type": "alarm",
      "x": 0,
      "y": 18,
      "width": 24,
      "height": 4,
      "properties": {
        "alarms": [
          "arn:aws:cloudwatch:us-east-1:123456789012:alarm:AppRunner-HighErrorRate",
          "arn:aws:cloudwatch:us-east-1:123456789012:alarm:AppRunner-P99-Latency-High",
          "arn:aws:cloudwatch:us-east-1:123456789012:alarm:AppRunner-HighCPU"
        ],
        "title": "Alarm Status"
      }
    }
  ]
}'

This dashboard uses a 24-column grid (standard CloudWatch layout width). Widgets are placed with x,y coordinates and width,height spanning multiple columns. The last widget displays alarm statuses inline, giving operators an at-a-glance health check.

Adding Custom Metrics to the Dashboard

Your application can publish custom metrics alongside App Runner's built-in ones. For example, if your service performs background jobs, you might track job queue depth or processing time. Here is how to publish a custom metric from your application code running inside App Runner:

import boto3
import time

cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

def publish_queue_depth(queue_size):
    cloudwatch.put_metric_data(
        Namespace='MyApp/BackgroundJobs',
        MetricData=[{
            'MetricName': 'QueueDepth',
            'Value': queue_size,
            'Unit': 'Count',
            'Timestamp': time.time(),
            'Dimensions': [{
                'Name': 'Environment',
                'Value': 'production'
            }]
        }]
    )
    print(f"Published QueueDepth={queue_size} to CloudWatch")

# Call this every 60 seconds from your background worker loop
publish_queue_depth(get_current_queue_size())

To include this custom metric in the dashboard, add another widget referencing the MyApp/BackgroundJobs namespace:

{
  "type": "metric",
  "x": 0,
  "y": 22,
  "width": 12,
  "height": 6,
  "properties": {
    "metrics": [
      [ "MyApp/BackgroundJobs", "QueueDepth", "Environment", "production", { "stat": "Maximum", "label": "Queue Depth" } ]
    ],
    "view": "timeSeries",
    "stacked": false,
    "region": "us-east-1",
    "title": "Background Job Queue Depth",
    "period": 300
  }
}

Now your dashboard unifies App Runner infrastructure metrics with application-level business metrics in a single view.

Automating Alarm and Dashboard Deployment with CloudFormation

For production systems, define alarms and dashboards as CloudFormation resources so they are version-controlled, reproducible, and deployed alongside your App Runner service. Here is a CloudFormation snippet that provisions an alarm and an SNS topic in one stack:

AWSTemplateFormatVersion: "2010-09-09"
Description: "App Runner monitoring stack"

Parameters:
  AppRunnerServiceArn:
    Type: String
    Description: ARN of the App Runner service to monitor
  NotificationEmail:
    Type: String
    Description: Email to notify when alarms fire

Resources:
  AlarmTopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
        - Endpoint: !Ref NotificationEmail
          Protocol: email

  HighErrorRateAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AppRunnerServiceArn}-HighErrorRate"
      AlarmDescription: "Error count exceeds threshold"
      Namespace: AWS/AppRunner
      MetricName: ErrorCount
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 3
      Threshold: 10
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: ServiceId
          Value: !Ref AppRunnerServiceArn
      AlarmActions:
        - !Ref AlarmTopic
      TreatMissingData: missing

  Dashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: !Sub "${AppRunnerServiceArn}-dashboard"
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "x": 0, "y": 0, "width": 24, "height": 6,
              "properties": {
                "metrics": [
                  [ "AWS/AppRunner", "RequestCount", "ServiceId", "${AppRunnerServiceArn}", { "stat": "Sum" } ]
                ],
                "view": "timeSeries",
                "region": "us-east-1",
                "title": "Request Count"
              }
            }
          ]
        }

Deploy with aws cloudformation deploy --template-file monitoring.yaml --stack-name apprunner-monitoring --parameter-overrides AppRunnerServiceArn=arn:aws:apprunner:us-east-1:123456789012:service/my-app/abc123def NotificationEmail=ops@example.com

Best Practices for App Runner Monitoring

After implementing metrics, alarms, and dashboards, adopt these practices to keep your monitoring effective as your service evolves:

Conclusion

AWS App Runner removes the operational burden of container orchestration, but it does not eliminate the need for observability. By leveraging the built-in CloudWatch metrics, constructing targeted alarms for error rate, latency, and resource utilization, and surfacing everything in a unified dashboard, you gain a complete picture of your service's health. The code examples in this tutorial — CLI commands, CloudFormation templates, and Python SDK snippets — give you a production-ready starting point that you can adapt to your own services. Start with the golden signals, alarm conservatively, and iterate as your application grows. With these practices in place, you will catch issues before your users do and spend less time firefighting and more time building.

🚀 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