How to Set Up a CloudWatch Dashboard to Monitor Your Application
You've deployed your application and now you're flying blind — checking CPU in one tab, hunting for error counts in another, and refreshing the console every few minutes hoping nothing is on fire. Setting up a CloudWatch dashboard gives you a single screen with every signal that matters, so you stop context-switching and start actually operating your application.
TL;DR: CloudWatch Dashboard Setup at a Glance
| Step | What You're Doing | Why It Matters |
|---|---|---|
| 1 | Create the dashboard | Establishes the named container for all widgets |
| 2 | Add metric widgets (CPU, memory) | Surfaces infrastructure-level signals |
| 3 | Add log-based error count widget | Surfaces application-level failure signals |
| 4 | Set time range and auto-refresh | Keeps the view operationally current |
| 5 | Share or embed the dashboard | Gives the team a single source of truth |
How CloudWatch Dashboards Work
A CloudWatch dashboard is a customizable console page that renders one or more widgets. Each widget queries CloudWatch Metrics or CloudWatch Logs Insights on a defined time range and refresh interval. Widgets are not persistent monitors — they are live queries rendered at view time. This means the dashboard itself generates no alarms and triggers no actions; it is purely a visualization layer on top of existing metric streams and log groups.
Metrics like CPUUtilization flow from EC2 (or ECS, Lambda, etc.) into CloudWatch automatically. Memory metrics, however, are not published by default for EC2 — they require the CloudWatch Agent running on the instance. Error counts typically come from either a custom metric your application publishes, or a CloudWatch Logs Insights query against your application log group.
(CPU, Network)"| CWMetrics["CloudWatch Metrics"] Agent["CloudWatch Agent"] -->|"mem_used_percent
disk_used_percent"| CWMetrics App["Application"] -->|"PutMetricData
(custom errors)"| CWMetrics App -->|"Log events"| LogGroup["CloudWatch Log Group"] CWMetrics --> Dashboard["CloudWatch Dashboard"] LogGroup -->|"Logs Insights query"| Dashboard EC2 -.->|"Agent runs on instance"| Agent
- EC2 / ECS / Lambda emit built-in metrics (CPU, network, invocations) automatically to CloudWatch Metrics.
- CloudWatch Agent must be installed and configured on EC2 to publish memory and disk metrics.
- Application logs are streamed to a CloudWatch Log Group, where Logs Insights queries can derive error counts.
- Dashboard widgets query both Metrics and Logs Insights at render time and display results in the configured visualization type.
Prerequisites Before You Build the CloudWatch Dashboard
Before adding widgets, confirm these data sources are actually flowing. A widget pointing at a metric that doesn't exist renders silently empty — no error, just a blank graph. That blank graph has burned more than one on-call rotation.
- CPU: Available by default for EC2 instances as
AWS/EC2namespace, metricCPUUtilization. - Memory: Requires the CloudWatch Agent. Metrics appear under the
CWAgentnamespace (default) with metric namemem_used_percent. - Error counts: Either a custom metric your app publishes via
PutMetricData, or derived from a log group via Logs Insights.
Verify your CloudWatch Agent is publishing memory metrics before building the widget:
aws cloudwatch list-metrics \
--namespace CWAgent \
--metric-name mem_used_percent \
--region us-east-1
If the response returns an empty Metrics array, the agent is not running or not configured correctly. Fix the agent before proceeding — the widget will not self-heal once you add it.
Step 1: Create the Dashboard
Creating the dashboard is a single API call. The dashboard body is a JSON document describing all widgets and their positions. You can start with an empty body and add widgets via the console, or define the entire layout in JSON from the start. The CLI approach shown here creates an empty dashboard you'll populate in subsequent steps.
aws cloudwatch create-dashboard \
--dashboard-name MyAppDashboard \
--dashboard-body '{"widgets":[]}' \
--region us-east-1
Dashboard names must be unique per account per region. If a dashboard with the same name already exists, this call overwrites it — there is no separate update command. The put-dashboard command is the upsert equivalent and behaves identically.
Step 2: Add a CPU Utilization Widget
Each widget is defined by its type, position on the grid, and properties. The dashboard grid is 24 columns wide. A widget at position x:0, y:0 with width:12, height:6 occupies the left half of the first row.
The full dashboard body below creates a CPU line graph for a specific EC2 instance. Replace i-0123456789abcdef0 with your actual instance ID.
🔽 Click to expand — CPU widget dashboard body JSON
aws cloudwatch put-dashboard \
--dashboard-name MyAppDashboard \
--dashboard-body '{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"title": "EC2 CPU Utilization",
"metrics": [
[ "AWS/EC2", "CPUUtilization", "InstanceId", "i-0123456789abcdef0" ]
],
"period": 60,
"stat": "Average",
"view": "timeSeries",
"region": "us-east-1"
}
}
]
}' \
--region us-east-1
Step 3: Add a Memory Utilization Widget
Memory metrics from the CloudWatch Agent include a dimension for the instance ID and hostname. The exact dimension names depend on your agent configuration. The default configuration uses InstanceId and ImageId alongside InstanceType. Verify the exact dimensions your agent publishes using list-metrics before hardcoding them in the widget definition.
aws cloudwatch list-metrics \
--namespace CWAgent \
--metric-name mem_used_percent \
--region us-east-1 \
--query 'Metrics[].Dimensions'
Once you've confirmed the dimension names and values, add the memory widget by updating the dashboard body to include both the CPU widget from Step 2 and the new memory widget. Place it at x:12, y:0 to position it in the right half of the first row.
🔽 Click to expand — CPU + Memory combined dashboard body JSON
aws cloudwatch put-dashboard \
--dashboard-name MyAppDashboard \
--dashboard-body '{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"title": "EC2 CPU Utilization",
"metrics": [
[ "AWS/EC2", "CPUUtilization", "InstanceId", "i-0123456789abcdef0" ]
],
"period": 60,
"stat": "Average",
"view": "timeSeries",
"region": "us-east-1"
}
},
{
"type": "metric",
"x": 12,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"title": "EC2 Memory Used (%)",
"metrics": [
[ "CWAgent", "mem_used_percent", "InstanceId", "i-0123456789abcdef0" ]
],
"period": 60,
"stat": "Average",
"view": "timeSeries",
"region": "us-east-1"
}
}
]
}' \
--region us-east-1
Step 4: Add an Application Error Count Widget
Error counts are where most dashboards fall short. Engineers add CPU and memory, call it done, and then spend 20 minutes during an incident correlating a CPU spike with application errors that were in a completely different tab. Put the error signal on the same screen.
If your application publishes a custom metric (e.g., MyApp/Errors with metric name ErrorCount), add it as a standard metric widget using Sum as the statistic. If errors are only in logs, use a Logs Insights widget instead.
Option A: Custom metric widget (if your app calls PutMetricData)
Add this widget object to the widgets array at position x:0, y:6:
{
"type": "metric",
"x": 0,
"y": 6,
"width": 12,
"height": 6,
"properties": {
"title": "Application Error Count",
"metrics": [
[ "MyApp/Errors", "ErrorCount" ]
],
"period": 60,
"stat": "Sum",
"view": "timeSeries",
"region": "us-east-1"
}
}
Option B: Logs Insights widget (if errors are in log groups)
The log widget type runs a Logs Insights query. Replace /aws/my-application with your actual log group name. This query counts log events containing the string ERROR per minute.
{
"type": "log",
"x": 0,
"y": 6,
"width": 12,
"height": 6,
"properties": {
"title": "Application Error Count (from logs)",
"query": "SOURCE '/aws/my-application' | filter @message like /ERROR/ | stats count(*) as ErrorCount by bin(1m)",
"region": "us-east-1",
"view": "timeSeries"
}
}
A Logs Insights widget is a live query, not a pre-aggregated metric. On large log volumes, the widget query can take several seconds to render and may incur Logs Insights query costs. For high-frequency dashboards, publishing a custom metric via
PutMetricDataand using a metric widget is more cost-efficient at scale.
Step 5: Set Auto-Refresh and Time Range
Dashboard time range and refresh interval are console-side settings, not stored in the dashboard body JSON. In the CloudWatch console, use the time range selector in the top-right corner to set a default view (e.g., last 3 hours) and enable auto-refresh at 1-minute or 10-minute intervals depending on your operational cadence.
For a shared operations dashboard that stays open on a wall monitor, set auto-refresh to 1 minute and the time range to the last 1 hour. This keeps the view operationally relevant without accumulating stale data.
Step 6: IAM Permissions Required
The IAM principal creating or viewing the dashboard needs the following permissions. Read-only viewers need only the Get and List actions. Dashboard authors need Put as well.
🔽 Click to expand — IAM policy for dashboard author
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudWatchDashboardReadWrite",
"Effect": "Allow",
"Action": [
"cloudwatch:GetDashboard",
"cloudwatch:ListDashboards",
"cloudwatch:PutDashboard",
"cloudwatch:DeleteDashboards"
],
"Resource": "arn:aws:cloudwatch::123456789012:dashboard/MyAppDashboard"
},
{
"Sid": "CloudWatchMetricsRead",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricData",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
],
"Resource": "*"
},
{
"Sid": "LogsInsightsRead",
"Effect": "Allow",
"Action": [
"logs:StartQuery",
"logs:GetQueryResults",
"logs:DescribeLogGroups"
],
"Resource": "*"
}
]
}
Note that cloudwatch:GetMetricData, cloudwatch:ListMetrics, and the Logs Insights actions require "Resource": "*" — resource-level restrictions are not supported for these actions. Verify current support in the AWS Service Authorization Reference before applying ARN-level restrictions.
The Blank Widget Trap: A Real Diagnostic Pattern
Here's a failure pattern that wastes real time: you build the dashboard, add the memory widget, and it renders blank. No error. No warning. Just an empty graph with a flat line at zero.
The instinct is to blame the widget configuration — wrong namespace, wrong metric name, wrong dimension. So you spend 15 minutes tweaking the widget JSON. The actual cause: the CloudWatch Agent on the instance stopped publishing after a system reboot because the agent service wasn't configured to start automatically.
The correct diagnostic sequence is to verify the data source first, then the widget. Run list-metrics against the exact namespace and metric name before touching the widget definition. If list-metrics returns nothing, the problem is upstream of the dashboard entirely.
aws cloudwatch get-metric-statistics \
--namespace CWAgent \
--metric-name mem_used_percent \
--dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
--start-time 2024-01-15T00:00:00Z \
--end-time 2024-01-15T01:00:00Z \
--period 300 \
--statistics Average \
--region us-east-1
If this returns an empty Datapoints array, the agent is the problem. The dashboard is fine.
for namespace + metric name"] CheckMetric -->|"Empty result"| UpstreamFix["Fix upstream source:
Agent / App / IAM"] CheckMetric -->|"Metric exists"| CheckData["Run get-metric-statistics
for recent time range"] CheckData -->|"Empty Datapoints"| AgentStopped["Agent stopped or
not publishing — fix agent"] CheckData -->|"Data present"| CheckWidget["Inspect widget JSON:
dimensions / namespace / stat"] CheckWidget --> Fixed["Correct widget config"]
- Start by confirming the metric exists in CloudWatch via
list-metrics. - If the metric exists, verify it has recent data points via
get-metric-statistics. - Only if data is confirmed present should you investigate the widget configuration itself.
- If no data exists, the fix is upstream: agent configuration, application instrumentation, or IAM permissions on the publishing side.
Verifying Your CloudWatch Dashboard via CLI
After building the dashboard, retrieve the stored body to confirm the widget definitions were saved correctly. This is especially useful when automating dashboard creation through CI/CD pipelines.
aws cloudwatch get-dashboard \
--dashboard-name MyAppDashboard \
--region us-east-1 \
--query 'DashboardBody'
The response returns the dashboard body as a JSON string. Pipe it through jq to inspect the widget count and positions:
aws cloudwatch get-dashboard \
--dashboard-name MyAppDashboard \
--region us-east-1 \
--query 'DashboardBody' \
--output text | python3 -m json.tool
Wrap-Up and Next Steps for Your CloudWatch Dashboard
A working CloudWatch dashboard with CPU, memory, and error count widgets gives you operational visibility without tab-switching. The key operational discipline is to verify data sources before building widgets — a blank widget is always a data pipeline problem, not a dashboard problem.
From here, consider:
- CloudWatch Alarms: Add alarms on the same metrics so the dashboard reflects alarm state visually. Alarm widgets can be added alongside metric widgets.
- Cross-account dashboards: CloudWatch supports cross-account observability, allowing a single dashboard to display metrics from multiple AWS accounts. See the CloudWatch cross-account observability documentation.
- Dashboard sharing: CloudWatch supports sharing dashboards with users who don't have AWS accounts via a generated link. See the dashboard sharing documentation.
- Infrastructure as Code: Define dashboards in CloudFormation (
AWS::CloudWatch::Dashboard) or Terraform to version-control your observability configuration.
Glossary
| Term | Definition |
|---|---|
| CloudWatch Namespace | A logical container for CloudWatch metrics. AWS services use namespaces like AWS/EC2; custom metrics use user-defined namespaces. |
| CloudWatch Agent | A software agent installed on EC2 instances to collect system-level metrics (memory, disk) and application logs not available through the default EC2 hypervisor metrics. |
| Logs Insights | CloudWatch's query engine for log data. Supports SQL-like syntax to filter, aggregate, and visualize log events from CloudWatch Log Groups. |
| Dashboard Widget | A single visualization panel on a CloudWatch dashboard. Types include metric (time-series graphs), log (Logs Insights results), alarm status, and text. |
| PutMetricData | The CloudWatch API action used by applications to publish custom metrics. Enables application-level signals (error rates, latency) to appear alongside infrastructure metrics. |
Comments
Post a Comment