Guide · AWS CloudWatch · Metric Streaming
CloudWatch Metric Streams for MCP Servers — Push Metrics to Datadog, S3, or Kinesis
CloudWatch Metric Streams continuously push all CloudWatch metric updates to a Kinesis Firehose delivery stream — or directly to an HTTP endpoint — without any polling, rate limits, or GetMetricData API calls. For MCP server teams who route observability data into Datadog, Grafana Cloud, New Relic, or a home-grown S3 data lake, Metric Streams replaces the polling-based CloudWatch integration that third-party agents use. The push model delivers metric updates within 2–3 minutes of their evaluation period (instead of 5–15 minutes for polled integrations), never hits the GetMetricData API rate limit at high metric volume, and costs $0.003 per 1,000 metric updates rather than paying per API call. See CloudWatch Alarms for MCP servers for threshold-based alerting on these same metrics, and CloudWatch Anomaly Detection for MCP servers for ML-based dynamic baselines.
TL;DR
Create a CloudWatch Metric Stream that includes your relevant namespaces (AWS/Lambda, AWS/SQS, McpServer) and points it at a Kinesis Firehose delivery stream. Set the output format to json (for S3 / custom consumers) or opentelemetry0.7 (for Datadog/New Relic native OTel ingestion). Apply an include filter so you only stream the namespaces you care about — otherwise you pay to stream every AWS metric in your account. Metric updates arrive at your destination within 2–3 minutes without GetMetricData API calls or rate-limit errors.
Metric Streams vs. polling-based integrations
Every major observability SaaS (Datadog, New Relic, Grafana Cloud, Dynatrace) offers a CloudWatch integration. Under the hood, those integrations work by calling GetMetricData or GetMetricStatistics on a schedule — typically every 5 or 10 minutes. This polling approach has three problems that compound as your AWS footprint grows:
| Dimension | Polling (GetMetricData) | Metric Streams (push) |
|---|---|---|
| Latency | 5–15 min lag (poll interval + evaluation period) | 2–3 min (evaluation period + ~1 min stream lag) |
| Rate limits | GetMetricData is rate-limited; large accounts hit throttling | No API calls — CloudWatch pushes to Firehose internally |
| Cost model | $0.01 per 1,000 GetMetricData data points requested | $0.003 per 1,000 metric updates streamed |
| Completeness | Only polls metrics you explicitly configure | All metrics in configured namespaces, automatically including new ones |
| Granularity | Limited by poll interval — often loses sub-5-min spikes | Every evaluation-period update, including 1-min high-resolution metrics |
For MCP server monitoring specifically, the latency improvement matters: a 2-minute spike in your Lambda Errors metric that triggers an AliveMCP probe failure will appear in your Datadog dashboard within 3 minutes with Metric Streams, versus potentially 15+ minutes with a polling integration that happens to check every 10 minutes and misses the spike entirely.
Output formats
Metric Streams supports two output formats. Choose based on your destination:
JSON format (for S3, custom consumers, Lambda processors)
// Each line in the Firehose batch is a JSON object:
{
"metric_stream_name": "mcp-metrics-stream",
"account_id": "123456789012",
"region": "us-east-1",
"namespace": "AWS/Lambda",
"metric_name": "Errors",
"dimensions": {
"FunctionName": "mcp-server-prod"
},
"timestamp": 1730362800000,
"value": {
"max": 3.0,
"min": 0.0,
"sum": 3.0,
"count": 1.0
},
"unit": "Count"
}
OpenTelemetry 0.7 format (for Datadog, New Relic, Grafana Cloud native OTel)
The opentelemetry0.7 format encodes metrics as a protobuf-encoded OTel ExportMetricsServiceRequest. Datadog, New Relic, and Grafana Cloud all have Firehose HTTP endpoint targets that accept this format directly — no translation Lambda needed.
For custom namespaces (like your McpServer namespace from EMF-emitted metrics), only the JSON format includes them. OpenTelemetry format is limited to AWS-defined namespaces in most third-party integrations.
Setting up a Metric Stream with namespace filters
Without a namespace include filter, the stream pushes every CloudWatch metric in your account — EC2, RDS, S3, and hundreds of other services you may not care about. Always apply an include filter that covers only the namespaces relevant to your MCP server stack:
# Create Firehose delivery stream first (pointing to S3 for this example)
aws firehose create-delivery-stream \
--delivery-stream-name mcp-metrics-to-s3 \
--delivery-stream-type DirectPut \
--s3-destination-configuration '{
"RoleARN": "arn:aws:iam::123456789012:role/firehose-to-s3",
"BucketARN": "arn:aws:s3:::mcp-metrics-bucket",
"Prefix": "cloudwatch-metrics/year=!{timestamp:yyyy}/month=!{timestamp:MM}/",
"BufferingHints": { "SizeInMBs": 64, "IntervalInSeconds": 60 },
"CompressionFormat": "GZIP"
}'
# Create the Metric Stream with include filters
aws cloudwatch put-metric-stream \
--name mcp-metrics-stream \
--firehose-arn arn:aws:firehose:us-east-1:123456789012:deliverystream/mcp-metrics-to-s3 \
--role-arn arn:aws:iam::123456789012:role/cloudwatch-metric-streams-to-firehose \
--output-format json \
--include-filters '[
{"Namespace": "AWS/Lambda"},
{"Namespace": "AWS/SQS"},
{"Namespace": "AWS/SNS"},
{"Namespace": "McpServer"},
{"Namespace": "AWS/ApplicationELB"}
]'
The IAM role for CloudWatch needs firehose:PutRecord and firehose:PutRecordBatch on the delivery stream ARN, plus a trust policy for streams.metrics.cloudwatch.amazonaws.com. The Firehose role needs s3:PutObject on the destination bucket (plus kms:GenerateDataKey if the bucket uses SSE-KMS).
CDK setup
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as firehose from 'aws-cdk-lib/aws-kinesisfirehose';
import * as destinations from '@aws-cdk-lib/aws-kinesisfirehose-destinations-alpha';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
const metricsBucket = new s3.Bucket(this, 'MetricsBucket', {
lifecycleRules: [{ expiration: cdk.Duration.days(90) }],
});
const deliveryStream = new firehose.DeliveryStream(this, 'MetricsFirehose', {
destinations: [new destinations.S3Bucket(metricsBucket, {
compression: destinations.Compression.GZIP,
dataOutputPrefix: 'cloudwatch-metrics/',
bufferingInterval: cdk.Duration.seconds(60),
})],
});
// CloudWatch Metric Stream — using raw CfnMetricStream (L1) because
// L2 construct is not yet stable in CDK as of late 2026
const streamRole = new iam.Role(this, 'MetricStreamRole', {
assumedBy: new iam.ServicePrincipal('streams.metrics.cloudwatch.amazonaws.com'),
});
deliveryStream.grantPutRecords(streamRole);
new cloudwatch.CfnMetricStream(this, 'McpMetricStream', {
firehoseArn: deliveryStream.deliveryStreamArn,
roleArn: streamRole.roleArn,
outputFormat: 'json',
includeFilters: [
{ namespace: 'AWS/Lambda' },
{ namespace: 'AWS/SQS' },
{ namespace: 'McpServer' },
],
name: 'mcp-metrics-stream',
});
Sending Metric Streams to Datadog via Firehose HTTP endpoint
Datadog supports Firehose HTTP endpoint targets that ingest Metric Streams natively, bypassing their CloudWatch polling integration entirely. This is the recommended setup for Datadog customers with more than ~500 active Lambda functions, where polling starts hitting GetMetricData rate limits.
# Datadog Firehose HTTP destination setup
# (Datadog provides the endpoint URL and API key from their AWS integration page)
aws firehose create-delivery-stream \
--delivery-stream-name mcp-metrics-to-datadog \
--delivery-stream-type DirectPut \
--http-endpoint-destination-configuration '{
"EndpointConfiguration": {
"Url": "https://awsmetrics-intake.datadoghq.com/v1/input",
"Name": "Datadog",
"AccessKey": "YOUR_DATADOG_API_KEY"
},
"RequestConfiguration": {
"ContentEncoding": "GZIP"
},
"BufferingHints": { "SizeInMBs": 4, "IntervalInSeconds": 60 },
"RetryOptions": { "DurationInSeconds": 60 },
"S3BackupMode": "FailedDataOnly",
"S3Configuration": {
"RoleARN": "arn:aws:iam::123456789012:role/firehose-backup",
"BucketARN": "arn:aws:s3:::mcp-metrics-backup"
},
"RoleARN": "arn:aws:iam::123456789012:role/firehose-to-datadog"
}'
# Then create Metric Stream pointing to this Firehose
aws cloudwatch put-metric-stream \
--name mcp-metrics-to-datadog \
--firehose-arn arn:aws:firehose:us-east-1:123456789012:deliverystream/mcp-metrics-to-datadog \
--role-arn arn:aws:iam::123456789012:role/cloudwatch-metric-streams-role \
--output-format opentelemetry0.7 \
--include-filters '[{"Namespace": "AWS/Lambda"}, {"Namespace": "McpServer"}]'
Note: use opentelemetry0.7 format for Datadog's Firehose endpoint — it is the format their ingestion pipeline expects. Use json format for S3 or custom Lambda consumers.
Statistic additions (percentiles in streams)
By default, Metric Streams stream only the standard statistics: min, max, sum, count. For MCP server latency analysis, you often want p99 or p95 in your analytics destination rather than computing them from the limited standard stats. Use the --statistics-configurations option to add percentile statistics to streamed metrics:
aws cloudwatch put-metric-stream \
--name mcp-metrics-stream \
--firehose-arn arn:aws:firehose:us-east-1:123456789012:deliverystream/mcp-metrics-to-s3 \
--role-arn arn:aws:iam::123456789012:role/cloudwatch-metric-streams-role \
--output-format json \
--include-filters '[{"Namespace": "AWS/Lambda"}, {"Namespace": "McpServer"}]' \
--statistics-configurations '[
{
"IncludeMetrics": [
{"Namespace": "AWS/Lambda", "MetricName": "Duration"},
{"Namespace": "McpServer", "MetricName": "McpToolDuration"}
],
"AdditionalStatistics": ["p50", "p95", "p99", "p99.9"]
}
]'
Additional percentile statistics cost the same per-update rate as standard statistics. Each metric update with 4 additional statistics counts as 5 updates at $0.003/1,000.
AliveMCP use case: multi-account probe metric aggregation
If AliveMCP runs probe workers across multiple AWS regions or accounts, Metric Streams provide a clean way to aggregate probe metrics into a central S3 data lake for cross-region health trend analysis:
- Each probe worker account streams
McpServernamespace metrics to its own regional Firehose, which delivers to a central S3 bucket in the management account (using cross-account bucket policies). - An Athena table over the S3 prefix enables SQL queries like
SELECT endpoint, AVG(value.max) as avg_p99 FROM probe_metrics WHERE metric_name = 'ProbeLatencyMs' GROUP BY endpoint. - CloudWatch dashboards in the management account use cross-account metric widgets to show a global probe-health heatmap without needing a separate APM SaaS subscription.
This is substantially cheaper than routing all probe telemetry through an APM SaaS at 100M+ probes/month — Metric Streams at $0.003/1,000 updates versus typical APM SaaS costs of $0.01–$0.05 per trace/span.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Metrics stop appearing at destination; stream state shows RUNNING | Firehose delivery failing silently — common when S3 bucket policy denies Firehose role or KMS key policy expired | Check Firehose monitoring tab for DeliveryToS3.DataFreshness; look at S3 error prefix for failed-records files; verify bucket policy and KMS key access |
| Custom namespace (McpServer) metrics not appearing in stream | EMF-emitted metrics take up to 15 minutes to appear in the CloudWatch Metrics namespace list after first emission; Metric Stream filter was created before the namespace existed | Verify the namespace appears in aws cloudwatch list-metrics --namespace McpServer; update the stream's include filter after confirming namespace exists |
| Metric updates arriving 10+ minutes late instead of 2–3 minutes | Firehose buffering hints set too high (default 300s / 5 min interval) | Reduce Firehose buffering interval to 60s and size to 64 MB for near-real-time delivery |
| Datadog not receiving metrics from Firehose HTTP endpoint | Wrong output format — Datadog OTel endpoint requires opentelemetry0.7, not json | Delete and recreate the Metric Stream with --output-format opentelemetry0.7; output format cannot be changed on an existing stream |
| GetMetricData API calls still coming from your SaaS even after enabling Metric Streams | Third-party integration still has its polling CloudWatch integration enabled in addition to Metric Streams | Disable the CloudWatch polling integration on the SaaS side; keep only the Firehose-based integration active |