← Back to DevBytes

Monitoring Fargate: Metrics, Alarms, and Dashboards

Getting Started with Fargate Monitoring

Amazon ECS Fargate is a serverless compute engine for containers that removes the need to provision, manage, and scale underlying infrastructure. While Fargate abstracts away the host layer, it does not remove the need for observability. In fact, because you lose direct access to the host OS and daemons, you must rely entirely on AWS-native tooling to understand how your containers are behaving. This tutorial covers everything you need to know about monitoring Fargate workloads: the built-in metrics, how to create alarms, how to build dashboards, and how to instrument your application code with custom metrics.

What Monitoring Means for Fargate

Monitoring a Fargate deployment involves collecting and acting on three layers of data:

All these feed into Amazon CloudWatch, where you can visualize them on dashboards and trigger alarms that notify you or auto-scale your service.

Why Monitoring Matters for Fargate

Without monitoring, you are flying blind. Fargate bills you per vCPU-second and per GB-second of memory reservation. An over-provisioned task burns money silently. An under-provisioned task gets throttled or OOM-killed, causing outages. Monitoring helps you:

Default Metrics: What You Get for Free

Every Fargate service automatically sends the following metrics to CloudWatch (in the AWS/ECS namespace) at one-minute granularity, at no extra cost:

These metrics are available under dimensions like ClusterName and ServiceName. The TaskCount metric is particularly important for tracking deployments and scaling behavior.

Viewing Default Metrics in the Console

Navigate to CloudWatch → Metrics → AWS/ECS. You can graph any of these by selecting the metric and choosing a statistic like Average, Sum, or Maximum over your preferred period.

Enabling Container Insights

Container Insights provides per-container granularity, including task-level breakdowns, disk I/O, and network statistics. It charges a small amount per metric observation. Enable it per-cluster.

Via the AWS Console

ECS → Clusters → select your cluster → "Update Cluster" → toggle "Container Insights" on.

Via AWS CLI

aws ecs update-cluster-settings \
  --cluster my-fargate-cluster \
  --settings name=containerInsights,value=enabled \
  --region us-east-1

Via CloudFormation

Resources:
  FargateCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: my-fargate-cluster
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

Via CDK (TypeScript)

const cluster = new ecs.Cluster(this, 'FargateCluster', {
  clusterName: 'my-fargate-cluster',
  containerInsights: true,
});

Via Terraform

resource "aws_ecs_cluster" "fargate" {
  name = "my-fargate-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

Once enabled, Container Insights populates the ECS/ContainerInsights namespace with metrics like task_cpu_utilization, task_memory_utilization, container_cpu_utilization, container_memory_utilization, network_rx_bytes, network_tx_bytes, and more. These appear within 2-3 minutes.

Building CloudWatch Alarms

Alarms watch a single metric (or a math expression combining several) and change state from OK to ALARM when a threshold is breached. They can send notifications via SNS, trigger EC2 actions, or feed into auto-scaling policies.

High CPU Alarm (CLI Example)

aws cloudwatch put-metric-alarm \
  --alarm-name "fargate-high-cpu" \
  --alarm-description "Alert when service CPU exceeds 80% for 5 minutes" \
  --namespace "AWS/ECS" \
  --metric-name "CPUUtilization" \
  --statistic "Average" \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --dimensions Name=ClusterName,Value=my-fargate-cluster Name=ServiceName,Value=my-service \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts

Memory Utilization Alarm with Missing Data Handling

aws cloudwatch put-metric-alarm \
  --alarm-name "fargate-high-memory" \
  --namespace "AWS/ECS" \
  --metric-name "MemoryUtilization" \
  --statistic "Maximum" \
  --period 300 \
  --evaluation-periods 2 \
  --threshold 85 \
  --comparison-operator GreaterThanThreshold \
  --treat-missing-data "breaching" \
  --dimensions Name=ClusterName,Value=my-fargate-cluster Name=ServiceName,Value=my-service \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts

The --treat-missing-data breaching flag is critical: if a Fargate task crashes and stops reporting metrics, the alarm will fire instead of silently staying green. Always think about missing data semantics when designing alarms for ephemeral container workloads.

Task Count Alarm (Detecting Crash Loops)

aws cloudwatch put-metric-alarm \
  --alarm-name "fargate-task-count-low" \
  --namespace "AWS/ECS" \
  --metric-name "TaskCount" \
  --statistic "Minimum" \
  --period 300 \
  --evaluation-periods 2 \
  --threshold 1 \
  --comparison-operator LessThanThreshold \
  --treat-missing-data "breaching" \
  --dimensions Name=ClusterName,Value=my-fargate-cluster Name=ServiceName,Value=my-service \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts

CDK Alarm Example (TypeScript)

import * as cw from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';

const alarmTopic = new sns.Topic(this, 'OpsAlerts');

new cw.Alarm(this, 'FargateCpuAlarm', {
  metric: new cw.Metric({
    namespace: 'AWS/ECS',
    metricName: 'CPUUtilization',
    statistic: 'Average',
    period: cdk.Duration.minutes(5),
    dimensionsMap: {
      ClusterName: cluster.clusterName,
      ServiceName: service.serviceName,
    },
  }),
  threshold: 80,
  evaluationPeriods: 2,
  comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
  treatMissingData: cw.TreatMissingData.BREACHING,
  alarmDescription: 'Triggers when Fargate CPU exceeds 80% for 10 minutes',
});
alarm.addAlarmAction(new cwActions.SnsAction(alarmTopic));

Creating Dashboards

CloudWatch dashboards let you pin multiple metric widgets onto a single pane of glass. You can create them via the console, the CLI, or infrastructure-as-code. Dashboards are JSON structures that define widgets, their layout, and their data sources.

Dashboard via CLI with Multiple Widgets

aws cloudwatch put-dashboard \
  --dashboard-name "Fargate-Overview" \
  --dashboard-body '{
  "widgets": [
    {
      "type": "metric",
      "x": 0, "y": 0, "width": 12, "height": 6,
      "properties": {
        "metrics": [
          ["AWS/ECS", "CPUUtilization", "ClusterName", "my-fargate-cluster", "ServiceName", "my-service", {"stat": "Average", "period": 60}],
          ["AWS/ECS", "MemoryUtilization", "ClusterName", "my-fargate-cluster", "ServiceName", "my-service", {"stat": "Average", "period": 60}]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "title": "CPU & Memory Utilization"
      }
    },
    {
      "type": "metric",
      "x": 12, "y": 0, "width": 12, "height": 6,
      "properties": {
        "metrics": [
          ["AWS/ECS", "TaskCount", "ClusterName", "my-fargate-cluster", "ServiceName", "my-service", {"stat": "Average", "period": 60}]
        ],
        "view": "timeSeries",
        "region": "us-east-1",
        "title": "Running Task Count"
      }
    },
    {
      "type": "metric",
      "x": 0, "y": 6, "width": 24, "height": 6,
      "properties": {
        "metrics": [
          ["ECS/ContainerInsights", "network_rx_bytes", "ClusterName", "my-fargate-cluster", "ServiceName", "my-service", {"stat": "Sum", "period": 60}],
          ["ECS/ContainerInsights", "network_tx_bytes", "ClusterName", "my-fargate-cluster", "ServiceName", "my-service", {"stat": "Sum", "period": 60}]
        ],
        "view": "timeSeries",
        "region": "us-east-1",
        "title": "Network Throughput (Container Insights)"
      }
    }
  ]
}'

CDK Dashboard Example

import * as cw from 'aws-cdk-lib/aws-cloudwatch';

const dashboard = new cw.Dashboard(this, 'FargateDashboard', {
  dashboardName: 'Fargate-Overview',
});

// CPU widget
dashboard.addWidgets(
  new cw.GraphWidget({
    title: 'CPU Utilization',
    left: [
      new cw.Metric({
        namespace: 'AWS/ECS',
        metricName: 'CPUUtilization',
        statistic: 'Average',
        period: cdk.Duration.minutes(1),
        dimensionsMap: {
          ClusterName: cluster.clusterName,
          ServiceName: service.serviceName,
        },
      }),
    ],
    width: 12,
    height: 6,
  })
);

// Memory widget
dashboard.addWidgets(
  new cw.GraphWidget({
    title: 'Memory Utilization',
    left: [
      new cw.Metric({
        namespace: 'AWS/ECS',
        metricName: 'MemoryUtilization',
        statistic: 'Average',
        period: cdk.Duration.minutes(1),
        dimensionsMap: {
          ClusterName: cluster.clusterName,
          ServiceName: service.serviceName,
        },
      }),
    ],
    width: 12,
    height: 6,
  })
);

// Task count widget
dashboard.addWidgets(
  new cw.SingleValueWidget({
    title: 'Running Tasks',
    metrics: [
      new cw.Metric({
        namespace: 'AWS/ECS',
        metricName: 'TaskCount',
        statistic: 'Average',
        period: cdk.Duration.minutes(1),
        dimensionsMap: {
          ClusterName: cluster.clusterName,
          ServiceName: service.serviceName,
        },
      }),
    ],
    width: 6,
    height: 4,
  })
);

Container Insights Deep Dive

When Container Insights is enabled, ECS also streams performance log events to CloudWatch Logs under the log group /aws/ecs/containerinsights//performance. These logs contain structured JSON records that you can query with CloudWatch Logs Insights.

Sample Container Insights Performance Log

{
  "type": "Container",
  "container_name": "web",
  "task_id": "abc123...",
  "cluster_arn": "arn:aws:ecs:...",
  "cpu_utilization": 12.5,
  "memory_utilization": 34.2,
  "memory_limit_mb": 512,
  "network": {
    "rx_bytes": 123456,
    "tx_bytes": 78901
  },
  "storage": {
    "read_bytes": 0,
    "write_bytes": 4096
  }
}

Querying with Logs Insights

fields @timestamp, container_name, cpu_utilization, memory_utilization, network.rx_bytes, network.tx_bytes
| filter type = "Container"
| sort @timestamp desc
| limit 50

This query returns the last 50 container-level observations. You can build dashboard widgets directly from Logs Insights queries, or use the structured metrics already aggregated by Container Insights in the ECS/ContainerInsights namespace.

Custom Metrics: The Embedded Metric Format

For application-level observability—request latency, error counts, order processing rates—you should emit custom metrics from your code. The most cost-effective and structured way is CloudWatch Embedded Metric Format (EMF). Your application writes JSON-formatted metric records to stdout (or a log stream), and CloudWatch automatically extracts and aggregates them as metric data points. You pay only for the log ingestion, not per-metric-observation API calls.

EMF with Python (inside a Fargate Task)

import json
import time
import random
from datetime import datetime

def emit_metric():
    # Construct an EMF blob
    emf_blob = {
        "_aws": {
            "Timestamp": int(time.time() * 1000),
            "CloudWatchMetrics": [
                {
                    "Namespace": "MyApplication",
                    "Dimensions": [["service", "environment"]],
                    "Metrics": [
                        {"Name": "RequestLatency", "Unit": "Milliseconds"},
                        {"Name": "RequestCount", "Unit": "Count"}
                    ]
                }
            ]
        },
        "service": "order-api",
        "environment": "production",
        "RequestLatency": random.randint(5, 200),
        "RequestCount": 1,
        "traceId": "trace-abc-123"
    }
    
    # Write as a single line to stdout
    print(json.dumps(emf_blob))

# Emit every 10 seconds in a real application
while True:
    emit_metric()
    time.sleep(10)

When your Fargate task logs this to stdout, the CloudWatch agent (or the FireLens log router if configured) picks it up. CloudWatch automatically creates the metrics RequestLatency and RequestCount in the MyApplication namespace, dimensioned by service and environment. You can then alarm and dashboard these just like any other metric.

EMF with Node.js (Express Middleware)

const os = require('os');

// EMF helper function
function emitMetric(name, value, unit, dimensions) {
  const blob = {
    _aws: {
      Timestamp: Date.now(),
      CloudWatchMetrics: [
        {
          Namespace: 'MyNodeApp',
          Dimensions: [Object.keys(dimensions)],
          Metrics: [{ Name: name, Unit: unit }]
        }
      ]
    },
    ...dimensions,
    [name]: value
  };
  process.stdout.write(JSON.stringify(blob) + '\n');
}

// Express middleware for request latency
function latencyMiddleware(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    const latencyMs = Date.now() - start;
    emitMetric('HttpLatency', latencyMs, 'Milliseconds', {
      route: req.path,
      method: req.method,
      host: os.hostname()
    });
  });
  next();
}

// In your Express app:
// app.use(latencyMiddleware);

The beauty of EMF is that it requires no SDK calls—just structured logging. It's ideal for high-throughput services where calling PutMetricData for every event would be throttled or expensive.

Using the CloudWatch Agent Sidecar for EMF

If you want richer metric collection (e.g., memory pressure, disk usage inside the container), you can run the CloudWatch agent as a sidecar in your task definition. Here is a task definition excerpt:

{
  "family": "my-service",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "my-app-image:latest",
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-service",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "app"
        }
      }
    },
    {
      "name": "cw-agent",
      "image": "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest",
      "essential": false,
      "secrets": [
        {
          "name": "CW_CONFIG",
          "valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/cw-agent-config"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-service",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "cw-agent"
        }
      }
    }
  ]
}

The CloudWatch agent reads its config from SSM Parameter Store, collects OS-level and custom metrics, and forwards them to CloudWatch. This is the recommended approach for production workloads that need detailed system metrics beyond what Container Insights provides.

PutMetricData API for Low-Volume Custom Metrics

For low-frequency metrics (e.g., deployment events, batch job completions), you can use the PutMetricData API directly from your application code or from a Lambda function that reacts to ECS events.

Python boto3 Example

import boto3
from datetime import datetime

cloudwatch = boto3.client('cloudwatch')

response = cloudwatch.put_metric_data(
    Namespace='MyApplication/Deployments',
    MetricData=[
        {
            'MetricName': 'DeploymentSuccess',
            'Timestamp': datetime.utcnow(),
            'Value': 1.0,
            'Unit': 'Count',
            'Dimensions': [
                {'Name': 'Service', 'Value': 'order-api'},
                {'Name': 'Environment', 'Value': 'production'}
            ]
        }
    ]
)

Keep in mind that PutMetricData has API limits (150 transactions per second per region per account) and a cost per 1,000 metrics delivered. For high-cardinality or high-frequency metrics, EMF is almost always the better choice.

Connecting Alarms to Auto-Scaling

Fargate Service Auto Scaling uses CloudWatch alarms under the hood. When you configure target tracking or step scaling policies, ECS creates and manages alarms for you. However, you can also build your own alarm-driven scaling using Application Auto Scaling with custom metrics.

Registering a Scalable Target (CLI)

aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/my-fargate-cluster/my-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 10

Attaching a Custom Metric Scaling Policy

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-fargate-cluster/my-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name "QueueDepthScaling" \
  --policy-type "StepScaling" \
  --step-scaling-policy-configuration '{
    "AdjustmentType": "PercentChangeInCapacity",
    "StepAdjustments": [
      {"MetricIntervalLowerBound": 0, "ScalingAdjustment": 10},
      {"MetricIntervalLowerBound": 100, "ScalingAdjustment": 50}
    ],
    "Cooldown": 300
  }'

Then create a CloudWatch alarm on your custom QueueDepth metric and link it to this policy via --alarm-actions pointing to the scaling policy ARN. This allows you to scale Fargate services on business metrics, not just CPU or memory.

Best Practices

Conclusion

Monitoring Fargate is not an afterthought—it's a first-class engineering concern that directly impacts reliability, cost, and developer velocity. By layering the free ECS metrics, Container Insights for per-task granularity, and Embedded Metric Format for application-level signals, you build a complete observability picture. Pair these metrics with well-designed alarms (with proper missing-data handling) and focused dashboards, and you transform Fargate from a black-box container runtime into a transparent, manageable platform. The code patterns shown here—CloudFormation, CDK, Terraform, Python, and Node.js—should give you everything you need to instrument your own Fargate workloads from day one.

🚀 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