← Back to DevBytes

Monitoring Route 53: Metrics, Alarms, and Dashboards

Understanding Route 53 Monitoring

Amazon Route 53 is a highly available and scalable DNS web service, but like any critical infrastructure component, it requires continuous observability. Monitoring Route 53 means tracking the health and performance of your DNS resolution, health checks, and routing policies through CloudWatch metrics, alarms, and dashboards. This gives you visibility into query volumes, failure rates, latency, and the operational state of your endpoints.

Route 53 monitoring operates across three interconnected layers: DNS query metrics for public and private hosted zones, health check metrics that track endpoint reachability, and routing control metrics when using Route 53 Application Recovery Controller. Each layer publishes data to CloudWatch, where you can build alarms, visualize trends, and trigger automated responses.

Why Route 53 Monitoring Matters

Without proper monitoring, DNS failures can silently cascade into application outages. A misconfigured health check might stop sending traffic to a healthy endpoint, or a sudden spike in DNS queries could indicate a DDoS attack. Monitoring helps you:

Key Route 53 Metrics in CloudWatch

Route 53 publishes metrics to CloudWatch in the AWS/Route53 namespace. These metrics fall into distinct categories, and understanding each one is essential before building alarms or dashboards.

DNS Query Metrics for Hosted Zones

For public hosted zones, Route 53 automatically publishes DNS query metrics aggregated across all edge locations. These are available in US East (N. Virginia) regardless of where your hosted zone is defined:

Health Check Metrics

Every Route 53 health check emits metrics to CloudWatch under the namespace AWS/Route53 with the dimension HealthCheckId. These are the most actionable metrics for alarming:

Latency-Based Routing Metrics

When you use latency-based routing, Route 53 relies on aggregated latency data from CloudWatch. The relevant metric is Latency in the AWS/Route53 namespace, though this data is typically consumed internally rather than alarmed against directly.

Setting Up Route 53 Health Checks

A health check defines how Route 53 verifies that an endpoint is reachable and responsive. You can create HTTP, HTTPS, or TCP health checks, and optionally combine them into calculated health checks for complex scenarios.

Creating a Basic HTTP Health Check via AWS CLI

The following command creates an HTTP health check that monitors api.example.com on port 443 using HTTPS, with a 30-second interval and a failure threshold of 3 consecutive failures:

aws route53 create-health-check \
    --caller-reference "api-health-check-$(date +%s)" \
    --health-check-config '{
        "IPAddress": "203.0.113.10",
        "Port": 443,
        "Type": "HTTPS",
        "ResourcePath": "/health",
        "FullyQualifiedDomainName": "api.example.com",
        "RequestInterval": 30,
        "FailureThreshold": 3,
        "MeasureLatency": true,
        "EnableSNI": true,
        "Regions": ["us-east-1", "us-west-2", "eu-west-1"]
    }'

The MeasureLatency flag ensures the TimeToFirstByte and TCPConnectionTime metrics populate in CloudWatch. The Regions array distributes health checkers across multiple geographic locations, reducing false positives from regional network issues.

Calculated Health Checks

A calculated health check aggregates multiple child health checks using AND/OR logic. This is critical for multi-region deployments where you want to fail over only when all primary regions are down:

aws route53 create-health-check \
    --caller-reference "calculated-primary-$(date +%s)" \
    --health-check-config '{
        "Type": "CALCULATED",
        "HealthThreshold": 2,
        "ChildHealthChecks": [
            "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "b2c3d4e5-f6a7-8901-bcde-f12345678901",
            "c3d4e5f6-a7b8-9012-cdef-123456789012"
        ]
    }'

With HealthThreshold set to 2, at least two of the three child health checks must pass for the overall check to be healthy. This prevents a single regional health checker glitch from triggering an unnecessary failover.

Building CloudWatch Alarms for Route 53

CloudWatch alarms translate Route 53 metrics into actionable notifications. The most common alarms focus on health check failures, but you should also consider DNS query anomalies and latency thresholds.

Health Check Failure Alarm

This alarm triggers when a health check reports a status of 0 for two consecutive evaluation periods, indicating the endpoint is down:

aws cloudwatch put-metric-alarm \
    --alarm-name "primary-endpoint-health-failure" \
    --alarm-description "Triggers when primary endpoint health check fails" \
    --namespace "AWS/Route53" \
    --metric-name "HealthCheckStatus" \
    --dimensions "Name=HealthCheckId,Value=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
    --statistic "Minimum" \
    --period 60 \
    --evaluation-periods 2 \
    --threshold 1.0 \
    --comparison-operator "LessThanThreshold" \
    --treat-missing-data "breaching" \
    --alarm-actions "arn:aws:sns:us-east-1:123456789012:route53-alerts" \
    --ok-actions "arn:aws:sns:us-east-1:123456789012:route53-alerts"

The treat-missing-data parameter set to breaching is critical here. If CloudWatch stops receiving data from the health checker (which itself indicates a problem), the alarm enters the ALARM state rather than staying green silently.

DNS Query Spike Alarm

Detect abnormal query volumes that might indicate a DDoS attack or misbehaving client:

aws cloudwatch put-metric-alarm \
    --alarm-name "dns-query-spike" \
    --alarm-description "Triggers when DNS queries exceed 10x baseline" \
    --namespace "AWS/Route53" \
    --metric-name "DNSQueries" \
    --dimensions "Name=HostedZoneId,Value=Z1234567890ABC" \
    --statistic "Sum" \
    --period 300 \
    --evaluation-periods 3 \
    --threshold 100000 \
    --comparison-operator "GreaterThanThreshold" \
    --treat-missing-data "notBreaching" \
    --alarm-actions "arn:aws:sns:us-east-1:123456789012:dns-security-alerts"

Latency Threshold Alarm for Health Checkers

Alert when endpoint latency exceeds SLA thresholds, even if the health check hasn't failed yet:

aws cloudwatch put-metric-alarm \
    --alarm-name "api-latency-high" \
    --alarm-description "API response time exceeds 2 seconds" \
    --namespace "AWS/Route53" \
    --metric-name "TimeToFirstByte" \
    --dimensions "Name=HealthCheckId,Value=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
    --statistic "Average" \
    --period 120 \
    --evaluation-periods 2 \
    --threshold 2000 \
    --comparison-operator "GreaterThanThreshold" \
    --unit "Milliseconds" \
    --treat-missing-data "ignore" \
    --alarm-actions "arn:aws:sns:us-east-1:123456789012:performance-alerts"

Creating CloudWatch Dashboards for Route 53

Dashboards provide a unified view of DNS health across all your hosted zones, health checks, and routing policies. You can create dashboards programmatically using the CloudWatch API, AWS CLI, or Infrastructure as Code tools.

Dashboard JSON Structure Overview

A CloudWatch dashboard is defined as a JSON string containing an array of widgets. Each widget specifies its type (line, metric, text, etc.), position, size, and the metrics to display. Here's the foundational structure:

{
  "widgets": [
    {
      "type": "metric",
      "x": 0,
      "y": 0,
      "width": 12,
      "height": 6,
      "properties": {
        "metrics": [
          [ "AWS/Route53", "DNSQueries", { "stat": "Sum" } ]
        ],
        "view": "timeSeries",
        "stacked": false,
        "region": "us-east-1",
        "period": 300,
        "title": "DNS Queries - Public Hosted Zone"
      }
    }
  ]
}

Complete Route 53 Monitoring Dashboard

The following CLI command creates a comprehensive dashboard with multiple widgets covering DNS queries, health check status, latency, and query type breakdown:

aws cloudwatch put-dashboard \
    --dashboard-name "Route53-Monitoring" \
    --dashboard-body '{
      "widgets": [
        {
          "type": "text",
          "x": 0,
          "y": 0,
          "width": 24,
          "height": 2,
          "properties": {
            "markdown": "# Route 53 Global Dashboard\n\n**Public Hosted Zone:** Z1234567890ABC | **Health Check:** primary-endpoint | **Region:** us-east-1"
          }
        },
        {
          "type": "metric",
          "x": 0,
          "y": 2,
          "width": 8,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "DNSQueries", { "HostedZoneId": "Z1234567890ABC", "stat": "Sum", "label": "Total Queries" } ]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "period": 300,
            "title": "DNS Query Volume"
          }
        },
        {
          "type": "metric",
          "x": 8,
          "y": 2,
          "width": 8,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "NoAnswer", { "HostedZoneId": "Z1234567890ABC", "stat": "Sum", "label": "NXDOMAIN/NoAnswer" } ]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "period": 300,
            "title": "Unanswered Queries"
          }
        },
        {
          "type": "metric",
          "x": 16,
          "y": 2,
          "width": 8,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "DNSQueriesByType", { "HostedZoneId": "Z1234567890ABC", "stat": "Sum", "label": "A" } ],
              [ ".", "DNSQueriesByType", { "HostedZoneId": "Z1234567890ABC", "DnsQueryType": "AAAA", "stat": "Sum", "label": "AAAA" } ],
              [ ".", "DNSQueriesByType", { "HostedZoneId": "Z1234567890ABC", "DnsQueryType": "CNAME", "stat": "Sum", "label": "CNAME" } ],
              [ ".", "DNSQueriesByType", { "HostedZoneId": "Z1234567890ABC", "DnsQueryType": "MX", "stat": "Sum", "label": "MX" } ]
            ],
            "view": "timeSeries",
            "stacked": true,
            "region": "us-east-1",
            "period": 300,
            "title": "Queries by Record Type"
          }
        },
        {
          "type": "metric",
          "x": 0,
          "y": 8,
          "width": 12,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "HealthCheckStatus", { "HealthCheckId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "stat": "Minimum", "label": "Health Status (1=Healthy, 0=Unhealthy)" } ]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "period": 60,
            "title": "Health Check Status",
            "yAxis": {
              "left": {
                "min": 0,
                "max": 1
              }
            }
          }
        },
        {
          "type": "metric",
          "x": 12,
          "y": 8,
          "width": 12,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "TimeToFirstByte", { "HealthCheckId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "stat": "p90", "label": "TTFB p90" } ],
              [ ".", "TimeToFirstByte", { "HealthCheckId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "stat": "Average", "label": "TTFB Avg" } ],
              [ ".", "TCPConnectionTime", { "HealthCheckId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "stat": "Average", "label": "TCP Connect" } ],
              [ ".", "SSLHandshakeTime", { "HealthCheckId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "stat": "Average", "label": "SSL Handshake" } ]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "period": 120,
            "title": "Health Check Latency Breakdown (ms)"
          }
        },
        {
          "type": "metric",
          "x": 0,
          "y": 14,
          "width": 12,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "HealthCheckPercentageHealthy", { "HealthCheckId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "stat": "Average", "label": "% Healthy Checkers" } ]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "period": 60,
            "title": "Percentage of Healthy Health Checkers",
            "yAxis": {
              "left": {
                "min": 0,
                "max": 100
              }
            }
          }
        },
        {
          "type": "metric",
          "x": 12,
          "y": 14,
          "width": 12,
          "height": 6,
          "properties": {
            "metrics": [
              [ "AWS/Route53", "ChildHealthCheckHealthyCount", { "HealthCheckId": "calculated-check-id", "stat": "Average", "label": "Healthy Children" } ]
            ],
            "view": "timeSeries",
            "stacked": false,
            "region": "us-east-1",
            "period": 60,
            "title": "Calculated Health Check - Healthy Children"
          }
        }
      ]
    }'

Using AWS CLI to Retrieve Current Dashboard

To iterate on an existing dashboard, retrieve its current definition, modify it, and push an update:

# Get current dashboard definition
aws cloudwatch get-dashboard \
    --dashboard-name "Route53-Monitoring" \
    --query 'DashboardBody' \
    --output text > dashboard-current.json

# Edit dashboard-current.json with your changes

# Push updated dashboard
aws cloudwatch put-dashboard \
    --dashboard-name "Route53-Monitoring" \
    --dashboard-body file://dashboard-current.json

Infrastructure as Code for Route 53 Monitoring

CloudFormation Template Example

Defining monitoring resources in CloudFormation ensures consistency across environments and enables reproducible deployments:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Route 53 Monitoring Stack - Health Checks and Alarms'

Parameters:
  EndpointIP:
    Type: String
    Description: IP address of the monitored endpoint
  EndpointFQDN:
    Type: String
    Description: Fully qualified domain name of the endpoint
  SnsTopicArn:
    Type: String
    Description: ARN of SNS topic for alarm notifications

Resources:
  PrimaryHealthCheck:
    Type: AWS::Route53::HealthCheck
    Properties:
      HealthCheckConfig:
        IPAddress: !Ref EndpointIP
        Port: 443
        Type: HTTPS
        ResourcePath: /health
        FullyQualifiedDomainName: !Ref EndpointFQDN
        RequestInterval: 30
        FailureThreshold: 3
        MeasureLatency: true
        EnableSNI: true
        Regions:
          - us-east-1
          - us-west-2
          - eu-west-1
      HealthCheckTags:
        - Key: Environment
          Value: Production
        - Key: Service
          Value: API

  HealthCheckAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${EndpointFQDN}-health-failure"
      AlarmDescription: !Sub "Health check failure for ${EndpointFQDN}"
      Namespace: AWS/Route53
      MetricName: HealthCheckStatus
      Dimensions:
        - Name: HealthCheckId
          Value: !Ref PrimaryHealthCheck
      Statistic: Minimum
      Period: 60
      EvaluationPeriods: 2
      Threshold: 1.0
      ComparisonOperator: LessThanThreshold
      TreatMissingData: breaching
      AlarmActions:
        - !Ref SnsTopicArn
      OKActions:
        - !Ref SnsTopicArn

  LatencyAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${EndpointFQDN}-high-latency"
      AlarmDescription: !Sub "TTFB exceeds 2000ms for ${EndpointFQDN}"
      Namespace: AWS/Route53
      MetricName: TimeToFirstByte
      Dimensions:
        - Name: HealthCheckId
          Value: !Ref PrimaryHealthCheck
      Statistic: Average
      Period: 120
      EvaluationPeriods: 2
      Threshold: 2000
      Unit: Milliseconds
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: ignore
      AlarmActions:
        - !Ref SnsTopicArn

  Route53Dashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: !Sub "${EndpointFQDN}-route53-monitoring"
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "x": 0,
              "y": 0,
              "width": 12,
              "height": 6,
              "properties": {
                "metrics": [
                  [ "AWS/Route53", "HealthCheckStatus", { "HealthCheckId": "${PrimaryHealthCheck}", "stat": "Minimum", "label": "Health Status" } ]
                ],
                "view": "timeSeries",
                "stacked": false,
                "region": "us-east-1",
                "period": 60,
                "title": "Health Check Status",
                "yAxis": { "left": { "min": 0, "max": 1 } }
              }
            },
            {
              "type": "metric",
              "x": 12,
              "y": 0,
              "width": 12,
              "height": 6,
              "properties": {
                "metrics": [
                  [ "AWS/Route53", "TimeToFirstByte", { "HealthCheckId": "${PrimaryHealthCheck}", "stat": "p90", "label": "TTFB p90" } ],
                  [ ".", "TimeToFirstByte", { "HealthCheckId": "${PrimaryHealthCheck}", "stat": "Average", "label": "TTFB Avg" } ]
                ],
                "view": "timeSeries",
                "stacked": false,
                "region": "us-east-1",
                "period": 120,
                "title": "Latency Metrics (ms)"
              }
            }
          ]
        }

CDK Example (TypeScript)

For teams using the AWS CDK, here's a reusable construct that encapsulates health check creation, alarming, and dashboard setup:

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

interface Route53MonitoringProps {
  endpointIp: string;
  endpointFqdn: string;
  resourcePath: string;
  alarmTopic: sns.ITopic;
  failureThreshold?: number;
  requestInterval?: number;
}

export class Route53MonitoringConstruct extends constructs.Construct {
  public readonly healthCheck: route53.CfnHealthCheck;
  public readonly healthAlarm: cloudwatch.Alarm;
  public readonly latencyAlarm: cloudwatch.Alarm;
  public readonly dashboard: cloudwatch.Dashboard;

  constructor(scope: constructs.Construct, id: string, props: Route53MonitoringProps) {
    super(scope, id);

    const failureThreshold = props.failureThreshold ?? 3;
    const requestInterval = props.requestInterval ?? 30;

    // Create the health check
    this.healthCheck = new route53.CfnHealthCheck(this, 'HealthCheck', {
      healthCheckConfig: {
        ipAddress: props.endpointIp,
        port: 443,
        type: 'HTTPS',
        resourcePath: props.resourcePath,
        fullyQualifiedDomainName: props.endpointFqdn,
        requestInterval: requestInterval,
        failureThreshold: failureThreshold,
        measureLatency: true,
        enableSni: true,
        regions: ['us-east-1', 'us-west-2', 'eu-west-1'],
      },
    });

    // Health check status alarm
    this.healthAlarm = new cloudwatch.Alarm(this, 'HealthAlarm', {
      alarmName: `${props.endpointFqdn}-health-failure`,
      alarmDescription: `Health check failure for ${props.endpointFqdn}`,
      metric: new cloudwatch.Metric({
        namespace: 'AWS/Route53',
        metricName: 'HealthCheckStatus',
        dimensionsMap: {
          HealthCheckId: this.healthCheck.attrHealthCheckId,
        },
        statistic: 'Minimum',
        period: cdk.Duration.seconds(60),
      }),
      threshold: 1,
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.BREACHING,
    });
    this.healthAlarm.addAlarmAction(new cloudwatch.SnsAction(props.alarmTopic));
    this.healthAlarm.addOkAction(new cloudwatch.SnsAction(props.alarmTopic));

    // Latency alarm
    this.latencyAlarm = new cloudwatch.Alarm(this, 'LatencyAlarm', {
      alarmName: `${props.endpointFqdn}-high-latency`,
      alarmDescription: `TTFB exceeds 2000ms for ${props.endpointFqdn}`,
      metric: new cloudwatch.Metric({
        namespace: 'AWS/Route53',
        metricName: 'TimeToFirstByte',
        dimensionsMap: {
          HealthCheckId: this.healthCheck.attrHealthCheckId,
        },
        statistic: 'Average',
        period: cdk.Duration.seconds(120),
      }),
      threshold: 2000,
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      unit: cloudwatch.Unit.MILLISECONDS,
      treatMissingData: cloudwatch.TreatMissingData.IGNORE,
    });
    this.latencyAlarm.addAlarmAction(new cloudwatch.SnsAction(props.alarmTopic));

    // Monitoring dashboard
    this.dashboard = new cloudwatch.Dashboard(this, 'Dashboard', {
      dashboardName: `${props.endpointFqdn.replace(/\./g, '-')}-route53-monitoring`,
      widgets: [
        new cloudwatch.SingleValueWidget({
          title: 'Current Health Status',
          metrics: [
            new cloudwatch.Metric({
              namespace: 'AWS/Route53',
              metricName: 'HealthCheckStatus',
              dimensionsMap: { HealthCheckId: this.healthCheck.attrHealthCheckId },
              statistic: 'Minimum',
              period: cdk.Duration.seconds(60),
            }),
          ],
          height: 6,
          width: 6,
        }),
        new cloudwatch.GraphWidget({
          title: 'Health Check Latency (ms)',
          left: [
            new cloudwatch.Metric({
              namespace: 'AWS/Route53',
              metricName: 'TimeToFirstByte',
              dimensionsMap: { HealthCheckId: this.healthCheck.attrHealthCheckId },
              statistic: 'p90',
              label: 'TTFB p90',
              period: cdk.Duration.seconds(120),
            }),
            new cloudwatch.Metric({
              namespace: 'AWS/Route53',
              metricName: 'TCPConnectionTime',
              dimensionsMap: { HealthCheckId: this.healthCheck.attrHealthCheckId },
              statistic: 'Average',
              label: 'TCP Connect',
              period: cdk.Duration.seconds(120),
            }),
            new cloudwatch.Metric({
              namespace: 'AWS/Route53',
              metricName: 'SSLHandshakeTime',
              dimensionsMap: { HealthCheckId: this.healthCheck.attrHealthCheckId },
              statistic: 'Average',
              label: 'SSL Handshake',
              period: cdk.Duration.seconds(120),
            }),
          ],
          height: 6,
          width: 12,
        }),
        new cloudwatch.GraphWidget({
          title: 'DNS Query Volume (5-min sum)',
          left: [
            new cloudwatch.Metric({
              namespace: 'AWS/Route53',
              metricName: 'DNSQueries',
              dimensionsMap: { HostedZoneId: 'Z1234567890ABC' },
              statistic: 'Sum',
              label: 'Total Queries',
              period: cdk.Duration.seconds(300),
            }),
          ],
          height: 6,
          width: 6,
        }),
      ],
    });

    // Output useful information
    new cdk.CfnOutput(this, 'HealthCheckId', {
      value: this.healthCheck.attrHealthCheckId,
      description: 'The ID of the Route 53 health check',
    });
  }
}

Terraform Example

For teams using Terraform, here's a complete monitoring module covering health checks, alarms, and a dashboard:

# variables.tf
variable "endpoint_ip" {
  type        = string
  description = "IP address of the monitored endpoint"
}

variable "endpoint_fqdn" {
  type        = string
  description = "FQDN of the monitored endpoint"
}

variable "alarm_sns_topic_arn" {
  type        = string
  description = "ARN of SNS topic for alarm notifications"
}

variable "hosted_zone_id" {
  type        = string
  description = "Route 53 hosted zone ID for DNS query monitoring"
}

# main.tf
resource "aws_route53_health_check" "primary" {
  fqdn              = var.endpoint_fqdn
  ip_address        = var.endpoint_ip
  port              = 443
  type              = "HTTPS"
  resource_path     = "/health"
  failure_threshold = 3
  request_interval  = 30
  measure_latency   = true
  enable_sni        = true
  regions           = ["us-east-1", "us-west-2", "eu-west-1"]

  tags = {
    Name        = "primary-endpoint-health-check"
    Environment = "production"
  }
}

resource "aws_cloudwatch_metric_alarm" "health_check_failure" {
  alarm_name          = "${var.endpoint_fqdn}-health-failure"
  alarm_description   = "Health check failure for ${var.endpoint_fqdn}"
  namespace           = "AWS/Route53"
  metric_name         = "HealthCheckStatus"
  statistic           = "Minimum"
  period              = 60
  evaluation_periods  = 2
  threshold           = 1.0
  comparison_operator = "LessThanThreshold"
  treat_missing_data  = "breaching"

  dimensions = {
    HealthCheckId = aws_route53_health_check.primary.id
  }

  alarm_actions = [var.alarm_sns_topic_arn]
  ok_actions    = [var.alarm_sns_topic_arn]
}

resource "aws_cloudwatch_metric_alarm" "high_latency" {
  alarm_name          = "${var.endpoint_fqdn}-high-latency"
  alarm_description   = "TTFB exceeds 2000ms for ${var.endpoint_fqdn}"
  namespace           = "AWS/Route53"
  metric_name         = "TimeToFirstByte"
  statistic           = "Average"
  period              = 120
  evaluation_periods  = 2
  threshold           = 2000
  unit                = "Milliseconds"
  comparison_operator = "GreaterThanThreshold"
  treat_missing_data  = "ignore"

  dimensions = {
    HealthCheckId = aws_route53_health_check.primary.id
  }

  alarm_actions = [var.alarm_sns_topic_arn]
}

resource "aws_cloudwatch_dashboard" "route53_monitoring" {
  dashboard_name = "${replace(var.endpoint_fqdn, ".", "-")}-route53-monitoring"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [
            ["AWS/Route53", "HealthCheckStatus", { HealthCheckId = aws_route53_health_check.primary.id, stat = "Minimum", label = "Health Status" }]
          ]
          view    = "timeSeries"
          stacked = false
          region  = "us-east-1"
          period  = 60
          title   = "Health Check Status"
          yAxis   = { left = { min = 0, max = 1 } }
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [
            ["AWS/Route53", "TimeToFirstByte", { HealthCheckId = aws_route53_health_check.primary.id, stat = "p90", label = "TTFB p90" }],
            [".", "TimeToFirstByte", { HealthCheckId = aws_route53_health_check.primary.id, stat = "Average", label = "TTFB Avg" }],
            [".", "TCPConnectionTime", { HealthCheckId = aws_route53_health_check.primary.id, stat = "Average", label = "TCP Connect" }]
          ]

🚀 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