← Back to DevBytes

Monitoring VPC: Metrics, Alarms, and Dashboards

Understanding VPC Monitoring

Monitoring your Virtual Private Cloud (VPC) is the practice of collecting, analyzing, and acting on operational data from your network infrastructure. In AWS, VPC monitoring encompasses network traffic analysis through VPC Flow Logs, performance metrics from NAT Gateways and VPN connections, subnet-level IP utilization, and custom metrics you define. This data flows into Amazon CloudWatch, where you can visualize trends, trigger automated responses, and build dashboards that give you a unified view of your network's health.

What VPC Monitoring Actually Is

At its core, VPC monitoring is about gaining visibility into three distinct layers of your network:

These signals are ingested by CloudWatch as time-series data points, where you can query them, build alarms with threshold-based triggers, and assemble custom dashboards for real-time operational awareness.

Why VPC Monitoring Matters

Without monitoring, your VPC is a black box. You cannot answer fundamental questions like "Is my NAT Gateway throttling outbound traffic?" or "Is someone scanning my internal subnets?" The consequences of inadequate monitoring include:

Effective monitoring transforms your VPC from opaque infrastructure into a transparent, predictable system where problems are caught before users notice them.

Key VPC Metrics You Should Track

AWS surfaces several categories of metrics natively. Understanding what each represents and when to alert on it is the foundation of a solid monitoring strategy.

VPC Flow Logs Metrics

Flow Logs themselves do not emit CloudWatch metrics directly—they publish log records to CloudWatch Logs, S3, or Kinesis Data Firehose. However, you can extract metrics from these logs using CloudWatch Logs Metric Filters. The most valuable derived metrics include:

Here is how you create a metric filter that counts rejected traffic and surfaces it as a CloudWatch metric:

aws logs put-metric-filter \
  --log-group-name "vpc-flow-logs" \
  --filter-name "RejectedTraffic" \
  --filter-pattern '{ ($.action = "REJECT") }' \
  --metric-transformations \
    '[{
        "metricName": "RejectedPacketCount",
        "metricNamespace": "VPC/FlowLogs",
        "metricValue": "1",
        "unit": "Count",
        "defaultValue": 0
      }]' \
  --region us-east-1

This command assumes your flow logs are in JSON format (the recommended configuration for metric extraction). The filter pattern uses CloudWatch Logs' JSON query syntax to match only records where the action field equals REJECT. Each matching log entry increments the metric by 1.

NAT Gateway Metrics

NAT Gateways emit the following native CloudWatch metrics under the AWS/NATGateway namespace every minute:

A critical alarm to configure is on ErrorPortAllocation, which indicates port starvation. Each NAT Gateway has roughly 55,000 ports available. When this metric is non-zero, connections are being refused:

aws cloudwatch put-metric-alarm \
  --alarm-name "NATGateway-PortExhaustion" \
  --alarm-description "Alert when NAT Gateway runs out of ports" \
  --namespace "AWS/NATGateway" \
  --metric-name "ErrorPortAllocation" \
  --statistic "Sum" \
  --period 300 \
  --evaluation-periods 1 \
  --threshold 1 \
  --comparison-operator "GreaterThanOrEqualToThreshold" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:ops-alerts" \
  --dimensions "Name=NatGatewayId,Value=nat-0abcd1234efgh5678" \
  --region us-east-1

VPN Connection Metrics

Site-to-Site VPN tunnels publish metrics under AWS/VPN namespace. These are essential for hybrid cloud architectures:

The TunnelState metric is the most important—you want to know immediately when a VPN endpoint fails. Here is a composite alarm that triggers when either tunnel goes down:

aws cloudwatch put-metric-alarm \
  --alarm-name "VPN-TunnelDown-Tunnel1" \
  --namespace "AWS/VPN" \
  --metric-name "TunnelState" \
  --statistic "Minimum" \
  --period 60 \
  --evaluation-periods 3 \
  --threshold 0 \
  --comparison-operator "LessThanOrEqualToThreshold" \
  --dimensions "Name=VpnId,Value=vpn-0abc1234" "Name=TunnelIpAddress,Value=203.0.113.25" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:vpn-alerts" \
  --region us-east-1

Repeat this for the second tunnel IP. A best practice is to configure these alarms with a composite alarm so you only page when both tunnels are down, avoiding noise from transient single-tunnel flaps.

Subnet IP Utilization Metrics

Introduced in 2022, these metrics appear under AWS/EC2 namespace with the SubnetId dimension:

These are crucial for container clusters (EKS, ECS) or auto-scaling groups where IP consumption grows rapidly. You can compute utilization percentage using CloudWatch Math expressions in dashboards or alarms:

{
  "metrics": [
    {
      "id": "used",
      "namespace": "AWS/EC2",
      "metricName": "SubnetIPUsage",
      "dimensions": { "SubnetId": "subnet-0abc1234" },
      "stat": "Average",
      "period": 300
    },
    {
      "id": "total",
      "namespace": "AWS/EC2",
      "metricName": "SubnetIPTotal",
      "dimensions": { "SubnetId": "subnet-0abc1234" },
      "stat": "Average",
      "period": 300
    },
    {
      "id": "utilization",
      "expression": "(used / total) * 100",
      "label": "IP Utilization %",
      "region": "us-east-1"
    }
  ]
}

Setting Up Alarms That Actually Work

Alarms transform raw metrics into actionable notifications. The difference between a useful alarm and noise lies in thoughtful configuration of thresholds, evaluation periods, and composite logic.

Static Threshold Alarms

The simplest alarm type triggers when a metric crosses a fixed value. Use these for binary conditions (tunnel state, port exhaustion errors) or metrics with predictable ceilings (CPU on a fixed-size fleet). The NAT Gateway port exhaustion alarm shown earlier is a classic static threshold example.

Anomaly Detection Alarms

For metrics with variable baselines—like NAT Gateway throughput that fluctuates with user activity—anomaly detection learns the expected pattern and alerts on deviations. This reduces false positives from static thresholds on dynamic workloads:

aws cloudwatch put-metric-alarm \
  --alarm-name "NATGateway-AnomalousThroughput" \
  --namespace "AWS/NATGateway" \
  --metric-name "BytesOutToDestination" \
  --statistic "Sum" \
  --period 300 \
  --evaluation-periods 2 \
  --threshold 2 \
  --comparison-operator "GreaterThanUpperThreshold" \
  --treat-missing-data "missing" \
  --dimensions "Name=NatGatewayId,Value=nat-0abcd1234efgh5678" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:ops-alerts" \
  --region us-east-1

The GreaterThanUpperThreshold comparison operator triggers when the metric exceeds the upper band of the anomaly detection model. The alarm automatically adapts to daily and weekly patterns.

Metric Math Alarms

Sometimes you need to alert on a derived value—like the ratio of rejected to accepted traffic. Metric math alarms use expressions to compute new metrics from existing ones:

aws cloudwatch put-metric-alarm \
  --alarm-name "HighRejectionRatio" \
  --namespace "VPC/FlowLogs" \
  --metric-name "RejectionRatio" \
  --statistic "Average" \
  --period 300 \
  --evaluation-periods 3 \
  --threshold 20 \
  --comparison-operator "GreaterThanThreshold" \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:security-alerts" \
  --metrics '[{
      "Id": "rejected",
      "MetricStat": {
        "Metric": { "Namespace": "VPC/FlowLogs", "MetricName": "RejectedPacketCount" },
        "Period": 300, "Stat": "Sum"
      },
      "ReturnData": false
    },
    {
      "Id": "accepted",
      "MetricStat": {
        "Metric": { "Namespace": "VPC/FlowLogs", "MetricName": "AcceptedPacketCount" },
        "Period": 300, "Stat": "Sum"
      },
      "ReturnData": false
    },
    {
      "Id": "ratio",
      "Expression": "(rejected / accepted) * 100",
      "Label": "RejectionPercentage"
    }]' \
  --region us-east-1

This alarm calculates the percentage of rejected packets relative to accepted ones and fires when it exceeds 20%, potentially indicating a misconfigured security group or an active threat.

Composite Alarms

Composite alarms reduce alert fatigue by combining multiple alarm states. They only trigger when all underlying alarms are in alarm state. This is ideal for VPN tunnels where you want notification only when both tunnels fail:

aws cloudwatch put-composite-alarm \
  --alarm-name "VPN-BothTunnelsDown" \
  --alarm-description "Triggers only when both VPN tunnels are down" \
  --alarm-rule '(ALARM("arn:aws:cloudwatch:us-east-1:123456789012:alarm:VPN-TunnelDown-Tunnel1") 
                 AND ALARM("arn:aws:cloudwatch:us-east-1:123456789012:alarm:VPN-TunnelDown-Tunnel2"))' \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:critical-alerts" \
  --region us-east-1

Building Effective Dashboards

CloudWatch Dashboards give you a unified, shareable view of your VPC's operational state. A well-designed dashboard tells a story: traffic flows top to bottom, from aggregate overviews to specific resource details.

Dashboard Architecture for VPC Monitoring

A production-grade VPC dashboard typically contains these sections:

Creating a Dashboard Programmatically

You can create dashboards via the AWS CLI, which is ideal for infrastructure-as-code pipelines. Here is a complete dashboard definition that covers VPC monitoring:

aws cloudwatch put-dashboard \
  --dashboard-name "VPC-Operations-Dashboard" \
  --dashboard-body '{
    "widgets": [
      {
        "type": "metric",
        "x": 0, "y": 0, "width": 12, "height": 6,
        "properties": {
          "metrics": [
            ["AWS/NATGateway", "BytesOutToDestination", { "stat": "Sum", "period": 300 }],
            ["AWS/NATGateway", "BytesInFromDestination", { "stat": "Sum", "period": 300 }]
          ],
          "view": "timeSeries",
          "stacked": false,
          "region": "us-east-1",
          "title": "NAT Gateway Aggregate Throughput",
          "yAxis": { "left": { "label": "Bytes/5min" } }
        }
      },
      {
        "type": "metric",
        "x": 0, "y": 6, "width": 6, "height": 6,
        "properties": {
          "metrics": [
            ["VPC/FlowLogs", "RejectedPacketCount", { "stat": "Sum", "period": 300 }],
            ["VPC/FlowLogs", "AcceptedPacketCount", { "stat": "Sum", "period": 300 }]
          ],
          "view": "timeSeries",
          "stacked": true,
          "region": "us-east-1",
          "title": "Flow Log Accept/Reject Breakdown"
        }
      },
      {
        "type": "metric",
        "x": 6, "y": 6, "width": 6, "height": 6,
        "properties": {
          "metrics": [
            ["AWS/VPN", "TunnelState", { "stat": "Minimum", "period": 60, "dimensions": { "VpnId": "vpn-0abc1234" } }]
          ],
          "view": "timeSeries",
          "region": "us-east-1",
          "title": "VPN Tunnel State (1=Up, 0=Down)"
        }
      },
      {
        "type": "metric",
        "x": 0, "y": 12, "width": 6, "height": 6,
        "properties": {
          "metrics": [
            ["AWS/NATGateway", "ActiveConnectionCount", { "stat": "Average", "period": 300 }],
            ["AWS/NATGateway", "ErrorPortAllocation", { "stat": "Sum", "period": 300 }]
          ],
          "view": "timeSeries",
          "region": "us-east-1",
          "title": "NAT Gateway Connections & Errors"
        }
      },
      {
        "type": "metric",
        "x": 6, "y": 12, "width": 6, "height": 6,
        "properties": {
          "metrics": [
            {
              "id": "used",
              "namespace": "AWS/EC2",
              "metricName": "SubnetIPUsage",
              "dimensions": { "SubnetId": "subnet-0abc1234" },
              "stat": "Average", "period": 300
            },
            {
              "id": "total",
              "namespace": "AWS/EC2",
              "metricName": "SubnetIPTotal",
              "dimensions": { "SubnetId": "subnet-0abc1234" },
              "stat": "Average", "period": 300
            },
            {
              "id": "utilization",
              "expression": "(used / total) * 100",
              "label": "IP Utilization %"
            }
          ],
          "view": "gauge",
          "region": "us-east-1",
          "title": "Subnet IP Utilization",
          "yAxis": { "left": { "min": 0, "max": 100 } }
        }
      }
    ]
  }' \
  --region us-east-1

This dashboard JSON defines five widgets arranged in a grid. The x/y coordinates position each widget, and the width/height control their span. The gauge widget for subnet utilization is particularly effective for capacity planning—it shows at a glance how close you are to exhaustion.

Adding Flow Log Dashboards with CloudWatch Logs Insights

For deeper traffic analysis, you can combine dashboards with CloudWatch Logs Insights queries that run against your Flow Logs. While not a native dashboard widget, you can pin Logs Insights results to dashboards. Here is a query you would run and pin to analyze top destination ports:

fields dstPort, @timestamp, action
| filter action = 'ACCEPT'
| stats count(*) as flowCount by dstPort
| sort flowCount desc
| limit 10

This query aggregates accepted flows by destination port, revealing your most active services. Pinning this to a dashboard gives you continuous visibility into traffic composition without running ad-hoc queries.

Infrastructure as Code: CloudFormation Example

For production environments, you should define monitoring alongside your infrastructure. Here is a CloudFormation snippet that creates a NAT Gateway with its associated alarm in a single template:

Resources:
  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  NatGatewayPortAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-NATGW-PortExhaustion"
      Namespace: AWS/NATGateway
      MetricName: ErrorPortAllocation
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: NatGatewayId
          Value: !Ref NatGateway
      AlarmActions:
        - !Ref AlertingSNSTopic

  VpnTunnelAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-VPN-TunnelDown"
      Namespace: AWS/VPN
      MetricName: TunnelState
      Statistic: Minimum
      Period: 60
      EvaluationPeriods: 3
      Threshold: 0
      ComparisonOperator: LessThanOrEqualToThreshold
      Dimensions:
        - Name: VpnId
          Value: !Ref VpnConnection
        - Name: TunnelIpAddress
          Value: "203.0.113.25"
      AlarmActions:
        - !Ref AlertingSNSTopic

  FlowLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/vpc/${AWS::StackName}-flow-logs"
      RetentionInDays: 30

  RejectedTrafficMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: !Ref FlowLogGroup
      FilterPattern: '{ ($.action = "REJECT") }'
      MetricTransformations:
        - MetricName: RejectedPacketCount
          MetricNamespace: VPC/FlowLogs
          MetricValue: "1"
          DefaultValue: 0

This template ensures every deployment gets monitoring by default. The alarm references the NAT Gateway resource directly, so the dimensions are always correct. The metric filter on the flow log group surfaces rejection metrics automatically.

Best Practices for VPC Monitoring

After implementing the technical components, these operational practices ensure your monitoring stays effective over time:

1. Enable Flow Logs on Every VPC

Enable VPC Flow Logs at the VPC level to capture all traffic. Use JSON format with the full set of fields. Publish to CloudWatch Logs for metric extraction or to S3 for long-term retention and Athena-based analysis. The minimal additional cost (roughly $0.50 per TB processed) is negligible compared to the visibility gained.

2. Build Alarms Before You Need Them

Configure alarms during infrastructure provisioning, not after an outage. A NAT Gateway port exhaustion alarm should exist from day one—retroactively adding it means you will eventually experience the failure before the alarm exists.

3. Use Composite Alarms for High-Availability Resources

For redundant resources like VPN tunnels (two per connection) or NAT Gateways (one per AZ), use composite alarms to reduce noise. Alert only when redundancy is exhausted, not when a single component fails over.

4. Implement Anomaly Detection for Dynamic Workloads

Static thresholds work poorly for traffic patterns that vary by time of day or day of week. Anomaly detection models learn these patterns automatically. Enable it on throughput metrics for NAT Gateways and VPN connections in production environments with variable load.

5. Monitor Subnet IP Utilization Proactively

Set alarms at 70% and 85% utilization on every subnet that hosts dynamic workloads (containers, auto-scaling groups). IP exhaustion causes hard failures that are difficult to recover from without subnet resizing or re-architecting.

6. Centralize Dashboards Across Accounts

If you operate multiple AWS accounts, use CloudWatch cross-account dashboards or tools like Grafana with CloudWatch data sources to build a single pane of glass. VPC monitoring scattered across account-specific dashboards creates fragmentation.

7. Tag Metrics for Cost Attribution

Use CloudWatch metric tags or consistent namespace conventions to attribute monitoring costs to specific teams or environments. NAT Gateway metrics are free, but custom metrics and Logs Insights queries have associated costs.

8. Test Your Alarm Pipeline

Periodically trigger alarms intentionally (e.g., by temporarily modifying a security group to generate rejects) to validate that notifications reach the correct channels and that runbooks are current. An alarm that fires into an unmonitored Slack channel provides no value.

Conclusion

VPC monitoring is not optional in any serious AWS deployment. The combination of VPC Flow Logs for traffic observability, native CloudWatch metrics for resource performance, and subnet utilization tracking for capacity planning gives you complete visibility into your network infrastructure. By setting up thoughtful alarms—using static thresholds for binary conditions, anomaly detection for variable workloads, and composite alarms for redundant resources—you catch problems before they impact users. Well-structured dashboards turn this data into operational context that your entire team can act on. The code examples in this guide provide a starting point; the real value comes from embedding these patterns into your infrastructure-as-code templates so that every VPC you deploy is observable from the moment it is created.

🚀 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