AWS CloudWatch · 2026-10-31 · CloudWatch Advanced arc
CloudWatch Advanced Patterns for MCP Server Observability: Metric Streams, Composite Alarms, Contributor Insights, and Anomaly Detection
AliveMCP probes every registered MCP endpoint every 60 seconds — at the scale of 100,000+ monitored servers, that is over 1,667 probe executions per second, each generating a structured log line, a Lambda duration sample, an error count increment, and a probe-throughput counter, all flowing into CloudWatch simultaneously. Standard CloudWatch metric alarms and GetMetricData API polling are not adequate for this volume or for the operational questions that matter most: which specific endpoint has been failing the most in the last hour, whether tonight's 3 AM latency spike is an actual incident or just the expected overnight quiet-period behavior, and whether multiple degraded signals constitute one real incident or three independent flaps that should not all page the same engineer at the same time. Four advanced CloudWatch capabilities close these gaps. Metric Streams replaces the polling-based CloudWatch integrations used by Datadog, Grafana Cloud, and New Relic with a push model that delivers metric updates within 2–3 minutes rather than 5–15 minutes, eliminates GetMetricData rate-limit errors entirely because CloudWatch pushes to Kinesis Firehose internally without any API calls from the observability SaaS, and costs $0.003 per 1,000 metric updates versus $0.01 per 1,000 GetMetricData data points — a 3× cost reduction that compounds when streaming high-resolution 1-minute metrics from hundreds of Lambda function dimensions; the output format choice is binary: json for S3 or custom Lambda consumers (works with custom namespaces like McpServer) or opentelemetry0.7 for Datadog and New Relic native OTel ingestion (custom namespaces typically not supported); percentile statistics like p95 and p99 must be explicitly added via --statistics-configurations because streams default to only min/max/sum/count; and a namespace include filter is mandatory unless you want to pay to stream every EC2, RDS, and S3 metric in the account, not just the Lambda and McpServer metrics you care about. Composite Alarms solve the dual failure mode of polling-era alerting — alert storms from correlated signals firing simultaneously, and false positives from legitimate maintenance activities — by wrapping multiple child alarms in a single boolean expression that evaluates immediately on any child state change rather than on a polling schedule; a probe failure manifests as a cluster of correlated signals (p99 latency alarm fires, error-rate alarm fires, and DLQ-depth alarm fires within seconds of each other), and without a composite OR alarm that owns the SNS notification, an on-call engineer receives three simultaneous pages for one incident; the AND rule variant closes the opposite gap — if you define a critical composite as ALARM(latency) AND ALARM(error-rate), it fires only when both signals are simultaneously degraded (a stronger signal of a real outage than either alone), while an OR-based warning composite covers the single-signal case and routes it to Slack without paging; maintenance suppression uses a dummy metric alarm with an unreachable threshold (9999999) that you manually flip to ALARM via set-alarm-state before deployments and back to OK after, combined with AND NOT ALARM("maintenance-window") in the composite rule, providing silent deploys without disabling the entire alerting stack. Contributor Insights answers the continuous operational question — "which MCP endpoints are failing the most right now?" — that Logs Insights requires you to re-run manually every time; a Contributor Insights rule runs continuously against the /mcp/probe-results log group, sampling 25% of log events (with an option for full 100% scan), and produces a real-time top-100 chart of contributors ranked by error count or accumulated latency without any query syntax or manual execution; the rule structure declares LogGroupNames, LogFormat (JSON or CLF), a Contribution block with Keys (the group-by fields — endpoint URL, registry, error class), an optional ValueOf for Sum aggregation, Filters for pre-aggregation event selection, and AggregateOn (Count or Sum); three practical rules for AliveMCP probe logs cover the top failing endpoints by error count, the top slow endpoints by accumulated latency sum (finds servers that are degraded but not erroring), and the top error classes broken down by registry (reveals which registries host the worst-quality servers); the GetInsightRuleReportCommand API enables programmatic consumption of top-N results for dashboard automation; and alarming on MaxContributorValue is possible in full-scan mode to catch single-endpoint error spikes that would be invisible in aggregate metrics. Anomaly Detection trains an ML model on up to two weeks of metric history using seasonal decomposition that captures hourly, daily, and weekly patterns, then creates a dynamic expected-value band whose width is controlled by a standard-deviations parameter — the critical scenario it solves is off-peak degradation that a static threshold misses entirely: a 500ms p99 at 3 AM when the normal value is 80ms is clearly broken (6× the baseline) but is well under a 1,000ms static threshold set for peak-hour traffic, while an anomaly detection alarm at stddev=2 fires because 500ms is far outside the expected nighttime band; the setup requires running put-anomaly-detector on the target metric to start model training, waiting at least 15 minutes for initialization, then creating a metric alarm with ANOMALY_DETECTION_BAND(m1, 2) in the Metrics array and GreaterThanUpperThreshold as the comparison operator; the lower-bound variant (LessThanLowerThreshold) detects worker failures visible as probe-throughput drops — if a probe worker handles 1,000 probes/minute and drops to 50, the lower-band alarm fires even if latency looks fine; ExcludedTimeRanges keeps deployment restarts and known maintenance windows from being learned as baseline behavior; and the model drift risk (where a sustained degradation eventually gets learned as "normal") is mitigated by always deploying a backup static threshold alarm alongside the anomaly detector. This guide synthesises all four CloudWatch advanced patterns. Pattern 1 covers Metric Streams — push vs polling architecture, output formats, namespace filtering, Datadog integration, percentile statistics, and multi-account aggregation — from CloudWatch Metric Streams for MCP Servers. Pattern 2 covers Composite Alarms — rule language evaluation, single-pager OR gates, maintenance suppression, and AND-based severity tiering — from CloudWatch Composite Alarms for MCP Servers. Pattern 3 covers Contributor Insights — rule structure, three practical MCP probe log rules, sampling vs full-scan cost tradeoff, and API-driven top-N reporting — from CloudWatch Contributor Insights for MCP Servers. Pattern 4 covers Anomaly Detection — ML model mechanics, off-peak detection superiority, ExcludedTimeRanges, throughput-drop detection, CDK limitations, and model drift mitigation — from CloudWatch Anomaly Detection for MCP Servers.
TL;DR
- Metric Streams — replace polling with push delivery at 2–3 min latency and $0.003/1k updates; use JSON for S3, opentelemetry0.7 for Datadog; always apply namespace include filters: Create a Kinesis Firehose delivery stream, then create a CloudWatch Metric Stream pointing at it with
--include-filters '[{"Namespace": "AWS/Lambda"}, {"Namespace": "McpServer"}]'. Choose--output-format jsonfor S3 or custom Lambda consumers (supports custom namespaces); chooseopentelemetry0.7for Datadog and New Relic native OTel ingestion (custom namespaces may not be forwarded). Add percentile statistics via--statistics-configurations— streams default to min/max/sum/count only. Reduce Firehose buffering to 60 seconds to get near-real-time delivery. For Datadog specifically: useopentelemetry0.7format with the Datadog Firehose HTTP endpoint, not the JSON format used for S3. - Composite Alarms — create individual metric alarms with no SNS actions, attach SNS only to the composite; use OR for health gates, AND for critical severity, and set-alarm-state for maintenance suppression: Create metric alarms for each signal (p99 latency, error rate, DLQ depth) with no
--alarm-actions. Create a composite OR alarm with--alarm-rule 'ALARM("mcp-p99-latency") OR ALARM("mcp-error-rate") OR ALARM("mcp-dlq-depth")'and attach the SNS pager there — one page per incident. For maintenance suppression: create a dummy metric alarm with threshold 9999999 (never fires automatically), then wrap composite rule withAND NOT ALARM("mcp-maintenance-window"); flip it manually withset-alarm-statebefore/after deployments. For severity tiering: AND alarm fires only when multiple signals simultaneously degraded (real outage); OR alarm routes single-signal events to Slack without paging. - Contributor Insights — create rules targeting /mcp/probe-results log group; run sampled mode for operational dashboards, full-scan for MaxContributorValue alarms: Define a rule with
LogGroupNames: ["/mcp/probe-results"],LogFormat: "JSON",Contribution.Keys: ["$.endpoint"],Contribution.Filters: [{"Match": "$.status", "In": ["error", "timeout", "auth_failure"]}],AggregateOn: "Count". Sampled mode (default 25%) costs $0.035/million log events for operational top-N dashboards — sufficient for trend identification. Full-scan mode (100%) costs $0.07/million but enables CloudWatch metric data for alarming onMaxContributorValueto catch single-endpoint error spikes. Query results programmatically withGetInsightRuleReportCommandfor automated health reports. Cardinality limit: 10,000 unique contributors per rule per hour — not a concern for MCP endpoint monitoring. - Anomaly Detection — use put-anomaly-detector to create a model, then alarm with ANOMALY_DETECTION_BAND(m1, 2) and GreaterThanUpperThreshold; always pair with a backup static threshold to catch model drift: Run
aws cloudwatch put-anomaly-detectorwith--stat p99andExcludedTimeRangesfor known maintenance windows. Wait 15+ minutes for model initialization (2 weeks for best accuracy). Create an alarm withANOMALY_DETECTION_BAND(m1, 2)math expression andGreaterThanUpperThreshold. UseLessThanLowerThresholdfor probe-throughput alarms that fire when worker failure causes a drop. Use CDK CfnAlarm (L1), not the L2 Alarm construct — the L2 construct has incomplete support for anomaly detection threshold metrics as of late 2026. Always deploy a parallel static threshold alarm at the absolute maximum acceptable value to catch model drift from sustained gradual degradation.
Pattern 1: Metric Streams — replacing polling with push delivery
Every major observability SaaS (Datadog, New Relic, Grafana Cloud, Dynatrace) ships a "CloudWatch integration" that calls GetMetricData on a schedule — typically every 5 or 10 minutes. This polling architecture has three fundamental problems for MCP server monitoring: the 5–15 minute lag means a probe-failure spike that lasts 4 minutes can disappear before the next poll; the rate limits on GetMetricData cause throttling on accounts with hundreds of Lambda function dimensions; and the cost of $0.01 per 1,000 GetMetricData data points exceeds the Metric Streams cost of $0.003 per 1,000 updates by 3×.
Push vs. polling comparison
| Dimension | Polling (GetMetricData) | Metric Streams (push) |
|---|---|---|
| Delivery latency | 5–15 min (poll interval + evaluation period) | 2–3 min (evaluation period + Firehose buffer) |
| Rate limits | GetMetricData throttled at high metric volume | No API calls — CloudWatch pushes internally |
| Cost | $0.01 per 1,000 data points requested | $0.003 per 1,000 metric updates streamed |
| Completeness | Only metrics you explicitly configure | All metrics in configured namespaces, including new ones |
| Custom namespaces | Yes — poll any namespace | Yes with JSON format; limited with opentelemetry0.7 |
Output format decision: JSON vs. OpenTelemetry 0.7
Metric Streams supports exactly two output formats, and the choice is not interchangeable:
json— newline-delimited JSON records in Firehose batches. Works with any S3-based consumer, custom Lambda processors, and the full set of CloudWatch namespaces including custom namespaces likeMcpServer. Use this for S3 data lakes queried with Athena, or for any consumer you control.opentelemetry0.7— protobuf-encoded OTel ExportMetricsServiceRequest. Datadog, New Relic, and Grafana Cloud all have Firehose HTTP endpoint targets that ingest this format natively without a translation Lambda. Custom namespaces (likeMcpServer) are typically not supported by third-party OTel ingestion endpoints — they map only the standard AWS namespaces they know about.
The failure mode here is format mismatch: if you create a Metric Stream with json format pointed at a Datadog Firehose HTTP endpoint, Datadog's ingestion pipeline rejects the payload silently, metrics stop appearing in Datadog, and the stream's state still shows RUNNING. Output format cannot be changed on an existing stream — you must delete and recreate it.
Namespace include filters and percentile statistics
Without a namespace include filter, a Metric Stream pushes every metric in your AWS account — EC2, RDS, S3, CloudFront, and dozens of other services. Always apply 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-role \
--output-format json \
--include-filters '[
{"Namespace": "AWS/Lambda"},
{"Namespace": "AWS/SQS"},
{"Namespace": "McpServer"}
]' \
--statistics-configurations '[
{
"IncludeMetrics": [
{"Namespace": "AWS/Lambda", "MetricName": "Duration"},
{"Namespace": "McpServer", "MetricName": "McpToolDuration"}
],
"AdditionalStatistics": ["p50", "p95", "p99"]
}
]'
The --statistics-configurations parameter adds percentile statistics to streamed records. Without it, the stream only includes min, max, sum, and count — you cannot compute p99 in the destination from those four values. Each additional statistic counts as an additional metric update at the same $0.003/1,000 rate.
Multi-account probe metric aggregation
If AliveMCP probe workers run across multiple AWS regions or accounts, Metric Streams enables central S3 aggregation without an APM SaaS subscription. Each probe worker account streams its McpServer namespace to a regional Firehose pointing at a central S3 bucket in the management account (via cross-account bucket policies). An Athena table over the central S3 prefix enables SQL queries across all regional probe data — for example, identifying which MCP endpoints are failing in eu-west-1 but healthy in us-east-1, which would indicate a regional DNS or routing issue rather than a server-side failure.
At 1,667 probe executions per second generating one McpToolDuration metric update each, Metric Streams cost for the McpServer namespace is approximately 1,667 × 60 × 60 × 24 × $0.003 / 1,000 = approximately $0.43/day — less than a single Datadog APM host per month for full metric streaming.
Pattern 2: Composite Alarms — health gates and maintenance suppression
A CloudWatch metric alarm evaluates one metric against one threshold. An MCP server failure manifests as a cluster: p99 latency rises, error count rises, and the dead-letter queue fills — all within the same minute. If each of these three alarms has its own SNS notification, the on-call engineer receives three pages simultaneously for one incident. Alert fatigue from correlated storms is one of the primary drivers of on-call engineers starting to ignore alerts — exactly the failure mode AliveMCP's customers pay to avoid at the server level.
Rule language and evaluation model
A composite alarm's rule is a boolean expression evaluated immediately whenever any child alarm changes state. The rule supports ALARM("name"), OK("name"), INSUFFICIENT_DATA("name"), AND, OR, NOT, and parentheses for grouping. Nesting depth is limited to 4 levels.
Critical difference from metric alarms: composite alarms have no evaluation period of their own. They do not poll on a schedule — they re-evaluate the moment a child alarm transitions. If three alarms fire within 5 seconds of each other, the composite alarm enters ALARM state once and triggers exactly one SNS notification.
Pattern 2a: Single pager for correlated signals (OR gate)
The canonical setup: create individual metric alarms with no SNS actions, then attach SNS only to the composite alarm:
# Individual alarms — no alarm-actions (composite owns the notification)
aws cloudwatch put-metric-alarm \
--alarm-name "mcp-p99-latency" \
--namespace McpServer --metric-name McpToolDuration \
--extended-statistic p99 \
--dimensions Name=FunctionName,Value=mcp-server-prod \
--period 60 --evaluation-periods 3 --threshold 2000 \
--comparison-operator GreaterThanOrEqualToThreshold \
--treat-missing-data notBreaching
aws cloudwatch put-metric-alarm \
--alarm-name "mcp-error-rate" \
--namespace McpServer --metric-name McpToolErrors \
--statistic Sum \
--dimensions Name=FunctionName,Value=mcp-server-prod \
--period 60 --evaluation-periods 2 --threshold 10 \
--comparison-operator GreaterThanOrEqualToThreshold \
--treat-missing-data notBreaching
# Composite OR gate — one SNS page per incident
aws cloudwatch put-composite-alarm \
--alarm-name "mcp-health-gate" \
--alarm-rule 'ALARM("mcp-p99-latency") OR ALARM("mcp-error-rate") OR ALARM("mcp-dlq-depth")' \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-oncall-pager \
--ok-actions arn:aws:sns:us-east-1:123456789012:mcp-oncall-pager
The --ok-actions on the composite sends a single "all clear" notification when the health gate recovers — one resolution notification matching the one incident notification.
Pattern 2b: Maintenance window suppression
Planned deployments legitimately break health checks. Without suppression, the on-call receives expected pages during known maintenance, trains the response "it's probably just a deploy," and eventually starts dismissing pages during real incidents. The solution is a dummy metric alarm that you manually flip:
# Dummy alarm — threshold 9999999 means it never fires automatically
# aws:cloudwatch:NumberOfMetricFilters is a real metric but will never reach this threshold
aws cloudwatch put-metric-alarm \
--alarm-name "mcp-maintenance-window" \
--namespace AWS/CloudWatch --metric-name NumberOfMetricFilters \
--statistic Average --period 3600 --evaluation-periods 1 \
--threshold 999999 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching
# Composite with suppression
aws cloudwatch put-composite-alarm \
--alarm-name "mcp-health-gate-suppressed" \
--alarm-rule 'ALARM("mcp-health-gate") AND NOT ALARM("mcp-maintenance-window")' \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-oncall-pager
# Before deployment
aws cloudwatch set-alarm-state --alarm-name mcp-maintenance-window \
--state-value ALARM --state-reason "Deploy $(date -u +%Y-%m-%dT%H:%M:%SZ)"
# After deployment
aws cloudwatch set-alarm-state --alarm-name mcp-maintenance-window \
--state-value OK --state-reason "Deploy complete"
Add a 5–10 second delay between set-alarm-state and starting the deployment. CloudWatch state propagation is near-instantaneous but not instantaneous — the composite re-evaluates as soon as the child state is received, so you want the maintenance window alarm to be in ALARM state before the health signals start degrading.
Pattern 2c: Severity tiering with AND logic
Some incidents warrant immediate paging; others need only a Slack message during business hours. AND logic distinguishes real outages (multiple signals simultaneously degraded) from transient flaps (single isolated signal):
# Critical: both latency AND errors degraded simultaneously — probable total outage
aws cloudwatch put-composite-alarm \
--alarm-name "mcp-critical-incident" \
--alarm-rule 'ALARM("mcp-p99-latency") AND ALARM("mcp-error-rate")' \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-pagerduty-critical
# Warning: any single signal degraded but not critical — notify Slack, no page
aws cloudwatch put-composite-alarm \
--alarm-name "mcp-warning-indicator" \
--alarm-rule '(ALARM("mcp-p99-latency") OR ALARM("mcp-error-rate") OR ALARM("mcp-dlq-depth")) AND NOT ALARM("mcp-critical-incident")' \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-slack-warnings
The mcp-warning-indicator fires when exactly one signal is degraded (any-OR minus the AND combination). This two-tier approach pages on multi-signal correlations (high-confidence real incidents) and notifies without paging on isolated signals (high-confidence transient flaps). Composite alarm cost: $0.50/alarm/month, versus $0.10/alarm/month for standard metric alarms — for a typical setup with 5 metric alarms and 3 composite alarms, the total is $2.00/month.
Pattern 3: Contributor Insights — continuous top-N error source identification
CloudWatch Logs Insights is excellent for ad-hoc investigation — "show me every probe result for api.mcp-broken.com between 3:40 and 3:55 AM." But answering "which MCP endpoints are failing the most right now?" requires you to remember to run a query, write the right aggregation syntax, wait for the query to complete, and repeat every time you want an update. For AliveMCP's operational dashboard, that operational question needs a continuously updating answer without manual intervention.
Contributor Insights vs. Logs Insights
| Question type | Logs Insights | Contributor Insights |
|---|---|---|
| Ad-hoc investigation: "what happened at 3:42 AM?" | Excellent — query historical data for any time range | Not suitable — shows top-N contributors, not individual events |
| Operational: "which endpoint is failing most right now?" | Requires repeated manual queries | Excellent — continuous real-time top-N, updated every minute |
| Dashboard: "top-10 error sources this week" | Can embed via metric math but expensive at high volume | Native top-N charts embeddable in CloudWatch dashboards |
| Alarm on a specific contributor exceeding a threshold | Not possible natively | Supported in full-scan mode via MaxContributorValue alarm |
| Cost at 100M log events/day | $0.005/GB scanned per query | $3.50/day sampled (25%), $7/day full-scan (100%) |
Rule structure
A Contributor Insights rule is a JSON document defining which log group to watch, how to identify contributors, and how to aggregate their contributions:
{
"Schema": { "Name": "CloudWatchLogRule", "Version": 1 },
"LogGroupNames": ["/mcp/probe-results"],
"LogFormat": "JSON",
"Contribution": {
"Keys": ["$.endpoint"], // group-by — up to 4 keys
"ValueOf": "$.latency_ms", // for Sum aggregation (omit for Count)
"Filters": [
{ "Match": "$.status", "In": ["error", "timeout", "auth_failure"] }
]
},
"AggregateOn": "Count" // Count or Sum (Sum requires ValueOf)
}
The Keys array defines the contributor identity. With Keys: ["$.endpoint"], each unique endpoint URL is a separate contributor. With Keys: ["$.registry", "$.error_class"], each registry+error-class pair is a contributor — enabling the "which registries host the worst-quality servers?" operational question. Cardinality limit: 10,000 unique contributor values per rule per hour, which is not a constraint for MCP endpoint monitoring with the current ~2,200 registered servers.
Three practical rules for AliveMCP probe logs
Assuming probe workers write structured JSON lines like {"endpoint": "https://api.mcp-example.com/mcp", "status": "error", "latency_ms": 8432, "error_class": "upstream_timeout", "registry": "mcp.so"} to /mcp/probe-results:
# Rule 1: Top failing endpoints by error count (standard operational dashboard)
aws cloudwatch put-insight-rule \
--rule-name "mcp-top-failing-endpoints" --rule-state ENABLED \
--rule-definition '{
"Schema": {"Name": "CloudWatchLogRule", "Version": 1},
"LogGroupNames": ["/mcp/probe-results"], "LogFormat": "JSON",
"Contribution": {
"Keys": ["$.endpoint"],
"Filters": [{"Match": "$.status", "In": ["error", "timeout", "auth_failure"]}]
}, "AggregateOn": "Count"
}'
# Rule 2: Top slow endpoints by accumulated latency (finds degraded-but-not-erroring servers)
aws cloudwatch put-insight-rule \
--rule-name "mcp-top-slow-endpoints" --rule-state ENABLED \
--rule-definition '{
"Schema": {"Name": "CloudWatchLogRule", "Version": 1},
"LogGroupNames": ["/mcp/probe-results"], "LogFormat": "JSON",
"Contribution": {
"Keys": ["$.endpoint"], "ValueOf": "$.latency_ms",
"Filters": [{"Match": "$.status", "NotIn": ["error"]}]
}, "AggregateOn": "Sum"
}'
# Rule 3: Error class by registry (reveals which registries host worst servers)
aws cloudwatch put-insight-rule \
--rule-name "mcp-error-class-by-registry" --rule-state ENABLED \
--rule-definition '{
"Schema": {"Name": "CloudWatchLogRule", "Version": 1},
"LogGroupNames": ["/mcp/probe-results"], "LogFormat": "JSON",
"Contribution": {
"Keys": ["$.registry", "$.error_class"],
"Filters": [{"Match": "$.status", "In": ["error"]}]
}, "AggregateOn": "Count"
}'
Querying results programmatically and alarming on MaxContributorValue
Rule results are available in the CloudWatch console and via the GetInsightRuleReport API for dashboard automation:
const { GetInsightRuleReportCommand } = require('@aws-sdk/client-cloudwatch');
const report = await cloudwatch.send(new GetInsightRuleReportCommand({
RuleName: 'mcp-top-failing-endpoints',
StartTime: new Date(Date.now() - 3600 * 1000),
EndTime: new Date(),
Period: 3600,
MaxContributorCount: 10,
Metrics: ['UniqueContributors', 'MaxContributorValue', 'Sum'],
OrderBy: 'Sum',
}));
report.Contributors.forEach(c => {
console.log(`${c.Keys[0]}: ${c.Approximation} errors in last hour`);
});
In sampled mode (default 25%), c.Approximation is a statistical estimate with ±15% accuracy at the 95th confidence level — sufficient for "which endpoints are the worst" operational ranking. For exact counts and for alarming, switch to full-scan mode by adding "InsightRuleSamplingPercentage": 100 to the rule definition. Full-scan mode publishes CloudWatch metrics under the CloudWatchLogs namespace, enabling a standard alarm on MaxContributorValue to fire when any single endpoint's error count exceeds a threshold — a signal that one bad actor endpoint is skewing overall probe health metrics.
Sampling vs. full-scan cost analysis
| Mode | Accuracy | Cost / million log events | CloudWatch metrics published? |
|---|---|---|---|
| Sampled (25%, default) | ~±15% for high-volume contributors | $0.035 effective (~$0.00875 at 25%) | No — results only in console and API |
| Full scan (100%) | Exact counts | $0.07 | Yes — enables MaxContributorValue alarming |
At AliveMCP scale (50M probe log events/day), sampled mode costs approximately $0.44/day per rule ($13/month); full-scan doubles this to $26/month. For a 3-rule setup, expect $39–$78/month depending on mode — modest for the operational visibility gained by replacing manual Logs Insights queries with continuously-running top-N charts.
Pattern 4: Anomaly Detection — ML baselines for off-peak degradation
Static threshold alarms have a fundamental problem for MCP server monitoring: the threshold that is meaningful during peak traffic is meaningless at night. A 1,000ms p99 threshold is reasonable during business hours when the baseline is 400ms — a 2.5× ratio. But at 3 AM when the baseline is 80ms, a server responding in 500ms is 6× its normal latency (clearly degraded) yet still under the 1,000ms threshold (alarm stays silent). An engineer who investigates a reported MCP failure at 3:15 AM will find a 500ms p99 that looks "fine" by the threshold, but which has been 6× the normal value for two hours.
How the model works
CloudWatch Anomaly Detection uses seasonal decomposition to model three components of metric behavior:
- Hourly seasonality — traffic patterns within a day (morning ramp, lunch peak, overnight quiet)
- Daily seasonality — weekday vs. weekend patterns (business hours vs. flat weekend traffic for B2B MCP servers)
- Trend — gradual drift in the baseline over weeks
The model requires at least 15 minutes of metric history to initialize and improves with up to 2 weeks of history. A single model covers one metric+statistic+dimension combination — a model on McpServer/McpToolDuration p99 FunctionName=mcp-server-prod is independent of a model on the same metric for FunctionName=mcp-server-staging. The standard deviations parameter controls band width: at stddev=2, approximately 95% of normal data points fall within the band (5% false positive rate); at stddev=3, ~99.7% fall within the band (fewer false positives but requires more pronounced anomalies to trigger).
Off-peak detection vs. static threshold comparison
| Scenario | Static threshold (1,000ms) | Anomaly detection (stddev=2) |
|---|---|---|
| Peak hour: baseline 400ms, spike to 1,200ms | Alarm fires (1,200 > 1,000) ✓ | Alarm fires (1,200 well above ~450ms expected) ✓ |
| Off-peak: baseline 80ms, spike to 500ms | No alarm (500 < 1,000) ✗ | Alarm fires (500ms is ~6× the expected ~85ms) ✓ |
| Gradual degradation: baseline drifts 80ms → 700ms over 3 weeks | Silent until 1,000ms ✗ | Model trend tracks the drift; alarm fires relative to recent baseline, then model normalizes — requires backup static alarm |
| Normal campaign traffic: p99 rises to 950ms | Alarms if threshold is below 950ms ✗ | Model adapts if sustained; short-term spike fires alarm ✓ |
Setup: anomaly detector and alarm configuration
# Step 1: Create anomaly detector model — starts training on last 2 weeks of history
aws cloudwatch put-anomaly-detector \
--namespace McpServer \
--metric-name McpToolDuration \
--dimensions '[{"Name": "FunctionName", "Value": "mcp-server-prod"}]' \
--stat p99 \
--configuration '{
"ExcludedTimeRanges": [
{"StartTime": "2026-10-15T02:00:00Z", "EndTime": "2026-10-15T04:00:00Z"}
],
"MetricTimezone": "America/New_York"
}'
# ExcludedTimeRanges: prevents deployment restart spikes from being learned as normal
# MetricTimezone: aligns day-of-week seasonality with your business timezone
# Step 2: Wait 15+ minutes for model initialization
# (check CloudWatch console — grey band appears on metric graph when model is ready)
# Step 3: Create anomaly detection alarm
aws cloudwatch put-metric-alarm \
--alarm-name "mcp-p99-anomaly" \
--metrics '[
{
"Id": "m1",
"MetricStat": {
"Metric": {
"Namespace": "McpServer", "MetricName": "McpToolDuration",
"Dimensions": [{"Name": "FunctionName", "Value": "mcp-server-prod"}]
},
"Period": 60, "Stat": "p99"
}
},
{
"Id": "ad1",
"Expression": "ANOMALY_DETECTION_BAND(m1, 2)",
"Label": "McpToolDuration p99 expected band"
}
]' \
--comparison-operator GreaterThanUpperThreshold \
--threshold-metric-id ad1 \
--evaluation-periods 3 \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-alerts
Detecting worker failures with LessThanLowerThreshold
Anomaly detection works in both directions. For probe throughput monitoring, a drop in probes-per-minute is the anomaly to detect — a worker that handles 1,000 probes/minute and drops to 50 is clearly broken, even if the remaining 50 probes show healthy latency. The latency metric looks fine; only throughput reveals the failure:
aws cloudwatch put-metric-alarm \
--alarm-name "mcp-probe-throughput-drop" \
--metrics '[
{
"Id": "m1",
"MetricStat": {
"Metric": {
"Namespace": "McpServer", "MetricName": "ProbesExecuted",
"Dimensions": [{"Name": "WorkerGroup", "Value": "probe-workers-prod"}]
},
"Period": 60, "Stat": "Sum"
}
},
{"Id": "ad1", "Expression": "ANOMALY_DETECTION_BAND(m1, 2)"}
]' \
--comparison-operator LessThanLowerThreshold \
--threshold-metric-id ad1 \
--evaluation-periods 5 \
--treat-missing-data breaching \
--alarm-actions arn:aws:sns:us-east-1:123456789012:mcp-alerts
Note --treat-missing-data breaching here — if the metric stops reporting entirely because the worker is completely dead, that absence should trigger the alarm. For latency alarms, notBreaching is correct because absent latency data means no traffic (not an incident), but for throughput alarms, absent data means the worker has stopped entirely.
CDK limitation and model drift mitigation
As of late 2026, the CloudWatch L2 CDK Alarm construct does not fully support anomaly detection threshold metrics. The comparisonOperator: ComparisonOperator.GREATER_THAN_UPPER_THRESHOLD option requires a thresholdMetricId pointing at the anomaly band expression, which the L2 construct cannot wire correctly. Use CfnAlarm (L1) with the metrics array matching the CLI structure above.
Model drift is the primary operational risk: a gradual degradation sustained for weeks gets incorporated into the model's baseline, causing the alarm to stop firing even though the metric is permanently elevated. Mitigate with two practices:
- Backup static threshold alarm — always deploy a parallel
GreaterThanOrEqualToThresholdalarm at the absolute maximum acceptable value (e.g., 3,000ms). Even if the anomaly detection model normalizes around 2,500ms, the static alarm still fires when further degradation reaches 3,000ms. - Exclude degradation periods — when you identify that a regression was introduced at a specific date, update
ExcludedTimeRangesto exclude that period from future training, so the model reverts toward the pre-regression baseline rather than continuing to learn the degraded state as normal.
Anomaly detection pricing: $0.30 per model per month. A typical MCP server setup with 3 models (p99 latency, error rate, probe throughput) plus 3 anomaly detection alarms costs $0.90 + $0.30 = $1.20/month — the same order of magnitude as a single standard metric alarm, making it cost-neutral if it catches even one off-peak degradation per month that a static threshold would miss.
Production checklist
| Pattern | Check | Common mistake |
|---|---|---|
| Metric Streams | Namespace include filter applied | Omitting include filter streams every metric in account — unexpected cost |
| Metric Streams | Output format matches destination | JSON format to Datadog HTTP endpoint — silent ingestion failure, stream shows RUNNING |
| Metric Streams | Percentile statistics explicitly added | Missing p99 in stream — cannot compute percentiles in destination from min/max/sum/count |
| Metric Streams | Firehose buffering ≤60 seconds | Default 300s buffering adds 5 min extra latency — defeats the purpose of Metric Streams |
| Composite Alarms | Individual metric alarms have no SNS actions | Attaching SNS to both child and composite — duplicate pages per incident |
| Composite Alarms | Maintenance dummy alarm threshold unreachable | Using a real metric that could naturally cross the threshold — false maintenance suppression |
| Composite Alarms | set-alarm-state runs before deployment starts | Starting deployment before state propagates — brief window where pages fire during suppression period |
| Contributor Insights | Full-scan mode enabled if MaxContributorValue alarming needed | Sampled mode rules do not publish CloudWatch metrics — alarms on contributor values never fire |
| Contributor Insights | Rule definition tested in console before automation | $. path syntax rejection at CLI — use file:// parameter or console rule editor first |
| Anomaly Detection | Using CfnAlarm (L1), not L2 Alarm construct | L2 construct cannot set thresholdMetricId — alarm may reference wrong band or fail deployment |
| Anomaly Detection | Backup static threshold alarm deployed alongside anomaly alarm | Relying solely on anomaly detection — model drift from gradual degradation creates false sense of security |
| Anomaly Detection | treat-missing-data=breaching on throughput drop alarms | notBreaching on throughput alarm — completely dead worker with no metric data shows as OK |