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:
- Detect anomalies early: A sudden spike in 4xx errors could mean a bad deployment or an expired authentication token. Alarms let you catch this before customers complain.
- Inform scaling decisions: App Runner scales automatically based on concurrent requests, but observing sustained high CPU utilization helps you decide whether to increase instance resources.
- Optimize cost: Dashboards reveal if you are over-provisioned (consistently low CPU/memory usage) or if a slow memory leak is creeping toward an outage.
- Troubleshoot latency: By correlating request count, request latency, and instance count, you can pinpoint whether slowdowns are caused by traffic surges or degraded backend dependencies.
- Meet SLA commitments: Alarms on latency and error rate let you prove uptime and performance to stakeholders.
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
- RequestCount – Total number of requests processed by the service. Unit: Count. Useful for traffic trending and anomaly detection.
- RequestLatency – Time in milliseconds from when the App Runner load balancer receives a request until it sends the response to the client. Available in multiple percentiles (p50, p90, p99, average). Unit: Milliseconds.
- ErrorCount – Number of requests that returned an HTTP 4xx or 5xx status code. Unit: Count.
- InstanceCount – Number of active instances (containers) running for the service. Unit: Count.
- CPUUtilization – Percentage of CPU units consumed by the service's instances. Unit: Percent.
- MemoryUtilization – Percentage of memory consumed by the service's instances. Unit: Percent.
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:
- period 300 – Aggregates 5 minutes of data into a single datapoint.
- evaluation-periods 3 – Requires 3 consecutive breaching datapoints (15 minutes of sustained errors) before firing.
- statistic Sum – Sums all errors in the period. Using
Averagehere would divide by the number of instances, masking the real error volume. - treat-missing-data missing – If data stops arriving (service down), the alarm stays in its current state rather than falsely transitioning to OK.
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:
- Start with the golden signals. For every App Runner service, instrument request count, latency (p50, p90, p99), error count, and instance count. These four signals cover availability, performance, and capacity.
- Use extended statistics for latency. Average latency hides tail problems. Always alarm on p95 or p99 latency; a handful of slow requests can indicate a degraded dependency that average values smooth over.
- Alarm on sustained breaches, not spikes. Use at least 2–3 evaluation periods and a period of 300 seconds (5 minutes). A single high datapoint is usually noise; sustained breaching indicates a real problem.
- Separate alarming levels. Create warning alarms (e.g., CPU > 70%) that notify a Slack channel, and critical alarms (e.g., error rate > 50%) that page on-call via PagerDuty. Composite alarms help route severity correctly.
- Treat missing data intentionally. For error counts, use
missing(do not auto-resolve). For latency on an idle service, usenotBreaching(no requests means no latency problem). Match the policy to the metric's semantics. - Version-control dashboards. Use
put-dashboardor CloudFormation to define dashboards as JSON. Avoid click-built dashboards that cannot be reproduced after a disaster. - Include alarm widgets on dashboards. A dashboard that shows metrics but not alarm status forces operators to switch contexts. Embed alarm widgets so the dashboard is a complete operational console.
- Publish custom metrics sparingly but meaningfully. Custom metrics cost money beyond the free tier. Focus on high-signal business metrics — checkout completions, signup conversions, background job backlog — that directly correlate to user experience.
- Set up anomaly detection for baseline metrics. CloudWatch anomaly detection learns the expected range of a metric over time and alarms on deviations. Enable it on request count and latency after you have 2–3 weeks of steady-state data.
- Review alarms quarterly. As traffic grows or code changes, thresholds drift. Schedule a recurring review to adjust alarm thresholds, retire stale alarms, and verify that notification endpoints still reach the right people.
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.