← Back to DevBytes

Monitoring EC2: Metrics, Alarms, and Dashboards

Understanding EC2 Monitoring

Monitoring EC2 instances is the continuous process of collecting, analyzing, and acting upon operational data from your virtual servers. At its core, EC2 monitoring revolves around three interconnected components: Metrics (the raw numerical data points), Alarms (automated threshold-based alerting rules), and Dashboards (visual representations that unify metrics and alarms into a single pane of glass).

Amazon CloudWatch serves as the central nervous system for this monitoring ecosystem. It automatically collects metrics from EC2 instances at no additional cost, stores them for 15 months, and provides APIs to retrieve and act upon that data. Understanding how these three pillars interact is essential for maintaining resilient, performant, and cost-efficient infrastructure.

What Are EC2 Metrics?

Metrics are time-series data points that represent a specific measurement about your instance at a given moment. CloudWatch divides these into two categories:

Default EC2 Metrics Explained

Every EC2 instance emits the following metrics to CloudWatch automatically. Understanding each one helps you diagnose issues before they become outages.

Why Monitoring Matters

Without monitoring, you are flying blind. Here are the concrete reasons why a robust monitoring strategy pays dividends:

Working with CloudWatch Alarms

What CloudWatch Alarms Are

A CloudWatch alarm watches a single metric over a defined time window and triggers an action when the metric breaches a threshold. Alarms can exist in three states: OK (metric is within bounds), ALARM (threshold breached), and INSUFFICIENT_DATA (not enough data points to evaluate). When an alarm transitions to the ALARM state, it can send notifications via SNS, trigger Auto Scaling policies, or execute EC2 actions like rebooting, stopping, or terminating an instance.

Creating an Alarm via AWS CLI

The following example creates an alarm that triggers when average CPU utilization exceeds 80% for two consecutive 5-minute evaluation periods, and sends a notification to an existing SNS topic.

aws cloudwatch put-metric-alarm \
  --alarm-name "HighCPUAlarm" \
  --alarm-description "Triggers when CPU exceeds 80% for 10 minutes" \
  --namespace "AWS/EC2" \
  --metric-name "CPUUtilization" \
  --dimensions "Name=InstanceId,Value=i-0abc1234def5678" \
  --statistic "Average" \
  --period 300 \
  --evaluation-periods 2 \
  --threshold 80.0 \
  --comparison-operator "GreaterThanThreshold" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:MyAlertTopic" \
  --treat-missing-data "missing"

Let's break down each parameter:

Creating a Composite Alarm

Composite alarms combine multiple metric alarms with logical AND/OR conditions. They reduce alert noise by requiring multiple conditions to be true simultaneously. For example, trigger only when both CPU is high AND status checks are failing.

aws cloudwatch put-composite-alarm \
  --alarm-name "CriticalInstanceFailure" \
  --alarm-description "CPU high AND status check failing" \
  --alarm-rule "ALARM(HighCPUAlarm) AND ALARM(StatusCheckAlarm)" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:CriticalAlertTopic" \
  --treat-missing-data "missing"

The --alarm-rule parameter accepts a mini DSL. You can nest conditions: "(ALARM(A) OR ALARM(B)) AND OK(C)" is valid. This flexibility lets you model complex failure scenarios without creating dozens of overlapping alarms.

EC2 Action Alarms: Automatic Recovery

One of the most powerful alarm features is the ability to automatically recover a failing instance. When a StatusCheckFailed_System alarm fires, CloudWatch can initiate an instance recovery—stopping and restarting the instance on new underlying hardware while preserving its instance ID, private IP addresses, and Elastic IPs.

aws cloudwatch put-metric-alarm \
  --alarm-name "AutoRecoverInstance" \
  --namespace "AWS/EC2" \
  --metric-name "StatusCheckFailed_System" \
  --dimensions "Name=InstanceId,Value=i-0abc1234def5678" \
  --statistic "Minimum" \
  --period 60 \
  --evaluation-periods 2 \
  --threshold 1.0 \
  --comparison-operator "GreaterThanThreshold" \
  --alarm-actions "arn:aws:automate:us-east-1:ec2:recover" \
  --treat-missing-data "missing"

The magic is in --alarm-actions "arn:aws:automate:us-east-1:ec2:recover". This special ARN instructs CloudWatch to execute the recovery workflow. Important prerequisites: the instance must use EBS-backed storage (not instance store), have a public or Elastic IP if that's required for connectivity, and be in a VPC where the subnet has sufficient capacity for a replacement instance.

Publishing Custom Metrics

Why Custom Metrics Are Essential

Default EC2 metrics give you host-level visibility, but they reveal nothing about what's happening inside the operating system or application. Memory pressure, disk space exhaustion, garbage collection pauses, request queue depths—these require custom instrumentation. CloudWatch allows you to publish any metric you want, and the process is straightforward.

Publishing a Custom Metric via AWS CLI

This example pushes a memory utilization metric to CloudWatch. You would typically run this from a cron job or daemon on the instance itself.

# Get memory usage percentage on a Linux instance
MEMORY_PERCENT=$(free | grep Mem | awk '{print ($3/$2) * 100.0}' | cut -d. -f1)

# Publish to CloudWatch
aws cloudwatch put-metric-data \
  --namespace "Custom/EC2" \
  --metric-name "MemoryUtilization" \
  --dimensions "Name=InstanceId,Value=i-0abc1234def5678" \
  --value ${MEMORY_PERCENT} \
  --unit "Percent" \
  --timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

Publishing High-Resolution Metrics with the SDK

For sub-minute granularity, use the --storage-resolution parameter or the equivalent SDK call. Here's an example using Python and boto3 to publish metrics every 10 seconds—useful for real-time latency monitoring.

import boto3
import time
import random
from datetime import datetime

cloudwatch = boto3.client('cloudwatch')

def publish_latency_metric(instance_id, value):
    cloudwatch.put_metric_data(
        Namespace='Custom/AppPerformance',
        MetricData=[
            {
                'MetricName': 'ApiResponseLatency',
                'Dimensions': [
                    {'Name': 'InstanceId', 'Value': instance_id},
                    {'Name': 'Endpoint', 'Value': '/api/checkout'}
                ],
                'Value': value,
                'Unit': 'Milliseconds',
                'Timestamp': datetime.utcnow(),
                'StorageResolution': 60  # 60 = 1-minute resolution, 1 = 1-second (high-res)
            }
        ]
    )

# Simulate publishing latency every 10 seconds for a demo
instance_id = 'i-0abc1234def5678'
while True:
    latency = random.gauss(mu=45, sigma=10)  # Simulated API latency ~45ms
    publish_latency_metric(instance_id, latency)
    time.sleep(10)

High-resolution metrics (StorageResolution: 1) allow you to set alarms with periods as low as 1 second, though they incur higher costs. Standard resolution metrics are free for the first 1 million API requests per month.

Using the CloudWatch Agent for System Metrics

Rather than scripting memory and disk metrics yourself, the unified CloudWatch Agent can collect a broad set of system-level metrics and logs from Linux and Windows instances. Install it and configure it with a JSON document.

# Install the CloudWatch agent on Amazon Linux 2
sudo yum install -y amazon-cloudwatch-agent

# Create the configuration file
sudo cat > /opt/aws/amazon-cloudwatch-agent/bin/config.json << 'EOF'
{
  "metrics": {
    "append_dimensions": {
      "InstanceId": "${aws:InstanceId}",
      "InstanceType": "${aws:InstanceType}"
    },
    "metrics_collected": {
      "mem": {
        "measurement": ["mem_used_percent", "mem_available", "swap_used_percent"],
        "metrics_collection_interval": 60
      },
      "disk": {
        "measurement": ["used_percent", "used", "total"],
        "resources": ["/", "/data"],
        "metrics_collection_interval": 60
      },
      "netstat": {
        "measurement": ["tcp_established", "tcp_time_wait"],
        "metrics_collection_interval": 60
      }
    }
  }
}
EOF

# Start the agent with the configuration
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json

The agent pushes these metrics to the CWAgent namespace by default. You can then build alarms and dashboards on memory, disk, swap, and network connection states—all without writing custom collection scripts.

Building CloudWatch Dashboards

What Dashboards Provide

CloudWatch dashboards are customizable, real-time visual canvases that display metric graphs, alarm status widgets, and text annotations. They persist across sessions, can be shared with direct URLs, and are ideal for NOC screens, on-call dashboards, and post-incident review sessions. A single dashboard can aggregate metrics from multiple regions and AWS services.

Creating a Dashboard via AWS CLI

Dashboards are defined as JSON strings containing widget definitions. The following creates a dashboard with CPU, network, and alarm status widgets for a specific instance.

aws cloudwatch put-dashboard \
  --dashboard-name "Production-EC2-Overview" \
  --dashboard-body '{
  "widgets": [
    {
      "type": "metric",
      "x": 0,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/EC2", "CPUUtilization", { "InstanceId": "i-0abc1234def5678" } ],
          [ ".", "NetworkIn", ".", "." ],
          [ ".", "NetworkOut", ".", "." ]
        ],
        "period": 300,
        "stat": "Average",
        "region": "us-east-1",
        "title": "Instance CPU and Network",
        "view": "timeSeries",
        "stacked": false
      }
    },
    {
      "type": "alarm",
      "x": 12,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "alarms": [
          "arn:aws:cloudwatch:us-east-1:123456789012:alarm:HighCPUAlarm",
          "arn:aws:cloudwatch:us-east-1:123456789012:alarm:AutoRecoverInstance"
        ],
        "title": "Active Alarms"
      }
    },
    {
      "type": "text",
      "x": 0,
      "y": 6,
      "width": 24,
      "height": 2,
      "properties": {
        "markdown": "## Production Instance Health\n\nLast updated: {{datetime}} | Instance: i-0abc1234def5678 | Auto-recovery enabled"
      }
    }
  ]
}'

Dashboard Widget Types

CloudWatch supports several widget types, each serving a distinct purpose:

Programmatic Dashboard Generation with Python

For large-scale deployments, manually crafting JSON is error-prone. Use boto3 to generate dashboards dynamically based on your infrastructure inventory.

import boto3

cloudwatch = boto3.client('cloudwatch')

def build_dashboard_for_instances(instance_ids, dashboard_name):
    widgets = []
    y_position = 0
    
    for idx, instance_id in enumerate(instance_ids):
        # CPU widget for each instance
        widgets.append({
            "type": "metric",
            "x": 0,
            "y": y_position,
            "width": 8,
            "height": 4,
            "properties": {
                "metrics": [
                    ["AWS/EC2", "CPUUtilization", {"InstanceId": instance_id}]
                ],
                "period": 300,
                "stat": "Average",
                "region": "us-east-1",
                "title": f"CPU - {instance_id}"
            }
        })
        
        # Network widget for each instance
        widgets.append({
            "type": "metric",
            "x": 8,
            "y": y_position,
            "width": 8,
            "height": 4,
            "properties": {
                "metrics": [
                    ["AWS/EC2", "NetworkIn", {"InstanceId": instance_id}],
                    [".", "NetworkOut", ".", "."]
                ],
                "period": 300,
                "stat": "Sum",
                "region": "us-east-1",
                "title": f"Network - {instance_id}"
            }
        })
        
        # Alarm status widget
        widgets.append({
            "type": "alarm",
            "x": 16,
            "y": y_position,
            "width": 8,
            "height": 4,
            "properties": {
                "alarms": [
                    f"arn:aws:cloudwatch:us-east-1:123456789012:alarm:HighCPU-{instance_id}"
                ],
                "title": f"Alarms - {instance_id}"
            }
        })
        
        y_position += 4
    
    dashboard_body = {"widgets": widgets}
    
    cloudwatch.put_dashboard(
        DashboardName=dashboard_name,
        DashboardBody=str(dashboard_body).replace("'", '"')  # Ensure valid JSON
    )
    
    print(f"Dashboard '{dashboard_name}' created with {len(instance_ids)} instance panels.")

# Example usage
instance_list = ['i-0abc1234def5678', 'i-1def2345abc6789', 'i-2ghi3456def7890']
build_dashboard_for_instances(instance_list, "Fleet-Health-Overview")

This approach scales to hundreds of instances. You can extend it to pull instance IDs from EC2 describe-instances, group them by Auto Scaling group tags, and create per-service dashboard slices automatically.

Using Metric Math on Dashboards

CloudWatch Metric Math lets you perform calculations on metrics and display the results as dashboard widgets. This is incredibly useful for computing aggregate views, ratios, and anomaly scores without publishing additional custom metrics.

{
  "type": "metric",
  "x": 0,
  "y": 0,
  "width": 12,
  "height": 6,
  "properties": {
    "metrics": [
      {
        "label": "Total Network Traffic (GB)",
        "expression": "(m1 + m2) / 1073741824",
        "id": "e1",
        "color": "#2ca02c"
      },
      {
        "label": "Network In",
        "id": "m1",
        "metricStat": {
          "metric": {
            "namespace": "AWS/EC2",
            "metricName": "NetworkIn",
            "dimensions": { "InstanceId": "i-0abc1234def5678" }
          },
          "period": 300,
          "stat": "Sum"
        },
        "visible": false
      },
      {
        "label": "Network Out",
        "id": "m2",
        "metricStat": {
          "metric": {
            "namespace": "AWS/EC2",
            "metricName": "NetworkOut",
            "dimensions": { "InstanceId": "i-0abc1234def5678" }
          },
          "period": 300,
          "stat": "Sum"
        },
        "visible": false
      }
    ],
    "view": "timeSeries",
    "title": "Combined Network Traffic (GB)",
    "region": "us-east-1"
  }
}

The expression field uses a simple query language. Common functions include SUM(METRICS()) for aggregating across multiple instances, METRICS("AWS/EC2", "CPUUtilization") to dynamically pull all matching metrics, arithmetic operations, and statistical functions like STDDEV and ANOMALY_DETECTION_BAND.

Best Practices for EC2 Monitoring

1. Enable Detailed Monitoring Selectively

Detailed monitoring (1-minute intervals) costs approximately $0.015 per instance-hour on top of the instance cost. Enable it for production instances where rapid detection matters—especially those behind Auto Scaling groups that need responsive scaling. For development and staging environments, standard 5-minute monitoring is usually sufficient.

2. Use Composite Alarms to Reduce Noise

A single metric spike rarely indicates a real problem. Design composite alarms that require multiple correlated signals. For example, trigger only when CPU is sustained above 90% AND application health checks are failing, rather than alerting on CPU alone. This dramatically reduces false-positive pages at 3 AM.

3. Implement the CloudWatch Agent Everywhere

Default EC2 metrics miss memory, disk space, and swap utilization. Deploy the CloudWatch Agent via your AMI baking process or user-data scripts so every instance reports these critical system metrics from the moment it launches. Store the agent configuration in Parameter Store for centralized management.

4. Set Up Status Check Alarms with Recovery Actions

This is perhaps the highest-leverage single action you can take. A StatusCheckFailed_System alarm paired with the EC2 recover action automatically replaces failed hardware without human intervention. The instance retains its configuration, EBS volumes, and network identity. Test this in staging first to ensure your applications survive the brief restart.

5. Build Tiered Dashboards

Create at least three dashboard layers:

6. Use Metric Math for Derived Insights

Instead of publishing a separate "requests per second per CPU percent" custom metric, compute it on the dashboard with Metric Math. This keeps your metric publishing lean and your dashboards flexible. You can iterate on derived metrics without redeploying metric collection code.

7. Tag Metrics with Meaningful Dimensions

When publishing custom metrics, include dimensions beyond just InstanceId. Add Environment, Service, Version, or Tenant dimensions. This enables cross-cutting views—you can graph latency for all instances of a particular service version regardless of which instance they're running on.

8. Monitor CPUCreditBalance for Burstable Instances

If you run T-family instances, credit exhaustion silently throttles CPU to baseline performance, which can cause mysterious slowdowns. Set a CloudWatch alarm on CPUCreditBalance when it drops below a critical threshold (e.g., 10 credits) and treat it as a warning that you may need to switch to an unlimited credit mode or a different instance family.

9. Automate Dashboard Creation

Manually maintaining dashboards as instances come and go is unsustainable. Use the SDK examples shown earlier to regenerate dashboards during deployments, or adopt Infrastructure as Code tools like CloudFormation or Terraform to define dashboards alongside your instance definitions. A dashboard that reflects only last week's topology is worse than no dashboard at all.

10. Set Alarms on Anomaly Detection Models

CloudWatch can automatically learn a metric's expected range using statistical models. Enable anomaly detection on critical metrics, then create alarms that trigger when values fall outside the expected band. This catches subtle issues that fixed-threshold alarms miss—like a gradual memory leak that never spikes above 90% but steadily climbs over days.

Conclusion

Effective EC2 monitoring is not merely about collecting data—it's about converting raw metrics into actionable intelligence. CloudWatch provides the foundational tools: automatic host metrics at no extra cost, a flexible alarm system capable of triggering recoveries and scaling actions, and dashboards that transform time-series data into visual narratives. By layering on custom metrics via the CloudWatch Agent or SDK-based instrumentation, you gain full-stack visibility from the hypervisor to the application. The practices outlined here—tiered dashboards, composite alarms, automated recovery, anomaly detection, and infrastructure-as-code dashboard generation—form a mature monitoring posture that reduces mean time to detection, minimizes alert fatigue, and ultimately keeps your EC2 fleet healthy and performant. Start with the fundamentals, iterate on your alarms as you learn your workload patterns, and treat your dashboards as living artifacts that evolve alongside your architecture.

🚀 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