Built-in detection (AWS)

CloudWatch rules over your estate inventory for accounts with no alarms — outages, and latency or faults above each service's own baseline; read-only, shadow first.

Concept · Updated 2026-10-28 · Describes release 0.1.164

Built-in detection watches the AWS resources in your estate inventory with a fixed set of CloudWatch rules, for accounts where nobody created alarms. It is off until a workspace owner turns it on, starts in shadow mode, and only reads: it lists metrics and reads their datapoints, and never creates an alarm, an anomaly detector or anything else in your account. No model decides anything here. A rule that fires sends an ordinary alert (source builtin) into the same pipeline as your own alerts: deduplication, correlation, grouping, the incident and, for a critical rule, an investigation.

Where it lives: Settings · Data in → Datasources (in the product: /portal/datasources) → Built-in detection (AWS).

What it watches

Thresholds and M-of-N come from AWS's Recommended alarms (Classic) list, the ALB and NLB metric pages and OpenSearch's recommended alarms. Ours marks a number no vendor source gives; every number and its reason is also in Thresholds and why. R26 to R30 (0.1.153) watch degradation as well as outages, where nobody created an alarm: a service answering slower or failing more than it normally does is an incident too. "Gate self" means the rule runs only where its own metric is published (a conditional metric); "gate resource" means it runs on every resource of its kind that published anything in the last 3 hours (its counts are published only when non-zero).

ID Resource Condition Window Severity Needs from the inventory
R01 EC2 instance StatusCheckFailed Max ≥ 1 2 of 2 × 5 min critical —
R02 EC2 instance StatusCheckFailed_AttachedEBS Max ≥ 1 10 of 10 × 1 min critical — (Nitro only, by gate)
R03 EBS volume, through its instance VolumeStalledIOCheck Max ≥ 1 10 of 10 × 1 min critical —
R04 EC2 instance CPUUtilization Avg > 80 % 3 of 3 × 5 min warning —
R05 RDS instance CPUUtilization Avg > 90 % 5 of 5 × 1 min warning —
R06 RDS instance (not Aurora) FreeStorageSpace Min < 10 % of allocated storage 5 of 5 × 1 min critical allocated storage; skipped with storage autoscaling
R07 RDS / Aurora instance FreeableMemory Avg < 25 % of class memory (Aurora 5 %) 15 of 15 × 1 min warning instance class (db.m*, db.r*, db.t*)
R08 RDS read replica ReplicaLag Max > 60 s 10 of 10 × 1 min warning replica source (never on a primary)
R09 RDS instance ReadLatency or WriteLatency p90 > 20 ms (ours) 5 of 5 × 1 min warning —
R10 ALB target group UnHealthyHostCount Min > 0 2 of 2 × 1 min (ours: the 1-minute period) critical —
R11 ALB HTTPCode_ELB_5XX_Count ÷ (RequestCount + HTTPCode_ELB_503_Count) > 5 %, ≥ 10 a minute (ours) 5 of 5 × 1 min critical —
R12 ALB target group HTTPCode_Target_5XX_Count ÷ RequestCount > 5 %, ≥ 10 a minute (ours) 5 of 5 × 1 min warning —
R13 NLB target group UnHealthyHostCount Min > 0 or HealthyHostCount Max < 1 2 of 2 × 1 min (ours) critical —
R14 Lambda function Errors ÷ Invocations > 5 %, ≥ 10 a minute (ours: the floor) 3 of 3 × 1 min warning —
R15 Lambda function Throttles Sum ≥ 1 5 of 5 × 1 min warning —
R16 API Gateway REST or HTTP API stage 5xx count ÷ Count > 5 %, ≥ 10 a minute (ours: the floor) 3 of 3 × 1 min critical the REST API's name
R17 API Gateway REST or HTTP API stage Latency p90 ≥ 2500 ms 5 of 5 × 1 min warning the REST API's name
R18 SQS queue ApproximateAgeOfOldestMessage Max ≥ 50 % of retention (ours) 15 of 15 × 1 min critical retention period
R19 DynamoDB table ReadThrottleEvents or WriteThrottleEvents Sum > 0 (ours) 5 of 5 × 1 min warning —
R20 EFS file system PercentIOLimit ≥ 100 % (General Purpose) or BurstCreditBalance ≤ 0 (Bursting) 15 of 15 × 1 min warning performance and throughput mode
R21 OpenSearch domain ClusterStatus.red Max ≥ 1 1 of 1 × 1 min critical —
R22 OpenSearch domain ClusterIndexWritesBlocked Max ≥ 1 1 of 1 × 5 min critical —
R23 OpenSearch domain FreeStorageSpace Min ≤ 25 % of node storage 1 of 1 × 1 min warning EBS volume size per node
R24 OpenSearch domain JVMMemoryPressure Max ≥ 95 % or OldGenJVMMemoryPressure Max ≥ 80 % 3 of 3 × 1 min warning —
R25 NAT gateway ErrorPortAllocation Sum > 0 15 of 15 × 1 min warning —
R26 Application Signals service on an EKS cluster Fault ÷ Latency SampleCount > the larger of 5 % (ours) and its baseline + 5 points (ours), ≥ 10 requests a minute (ours) 3 of 3 × 5 min (ours) warning the EKS cluster
R27 Application Signals service on an EKS cluster Latency p90 > the larger of its baseline × 1.5 and baseline + 100 ms (ours), ≥ 10 requests a minute (Latency SampleCount, ours) 3 of 3 × 5 min (ours) warning the EKS cluster
R28 ALB TargetResponseTime p95 (ours) > the larger of its baseline × 1.5 and baseline + 100 ms (ours), ≥ 10 requests a minute (RequestCount, ours) 3 of 3 × 5 min (ours) warning —
R29 EKS cluster (Container Insights) cluster_failed_node_count Max ≥ 1 (ours) 5 of 5 × 1 min warning the EKS cluster
R30 Pod of an EKS cluster (Container Insights) pod_number_of_container_restarts Sum ≥ 1 in each period (ours) 3 of 3 × 5 min (ours) warning the EKS cluster
R31 Kubernetes workload (kubectl) a container of one of its pods waits in CrashLoopBackOff, or restarted 3 times or more (ours) and crashed again since the previous read each poll warning a kubectl connection
R32 Kubernetes workload (kubectl) a container of one of its pods terminated OOMKilled since the previous read each poll warning a kubectl connection
R33 Kubernetes workload (kubectl) a pod in phase Pending for more than 10 minutes (ours) each poll warning a kubectl connection
R34 Kubernetes node (kubectl) Ready condition not True for more than 5 minutes each poll warning a kubectl connection
R35 EC2 instance an instance-retirement, instance-stop, instance-reboot, system-reboot or system-maintenance event AWS scheduled, not completed or canceled hourly info (P4, ours) —
R36 RDS DB instance a pending maintenance action with a date (the next maintenance window, applied automatically after a date, or forced on a date) hourly info (P4, ours) —
R37 AWS service in a region an open AWS Health issue for a service and region where the estate has an in-scope resource of that service each poll error (P2) any resource with an ARN

Each row on the card links to its source page. A rule AWS says not to apply is skipped and the card says why (for example "storage autoscaling on", "not a read replica", "retention not read").

Baselines (R26, R27, R28)

Latency and fault rates have no number that fits every service: 300 ms is normal for one API and an outage for another. These three rules compare each series with its own baseline: the 95th percentile of its 5-minute values over the last 7 days. A week holds the weekly cycle, and the 95th percentile lets one bad hour of it pass without hiding a regression. The threshold is the larger of two floors, so neither a fast service's jitter nor a slow service's normal swing alerts: for latency, 1.5 × the baseline and the baseline + 100 ms; for the fault rate, 5 % and the baseline + 5 points. AWS's own TargetResponseTime alarm says the threshold "depends on your situation" and to "analyze the historical behavior of this metric": the baseline is that history. A 5-minute period under 10 requests a minute is no evidence, for the baseline or the rule: a low-traffic service's p90 is one slow request.

An alert on an inventory resource (an instance, a load balancer, a database …) carries the resource's own service as its service: the first of its service, app, application or Name tags (keys in any case), else the resource's name. An instance tagged service=web therefore joins web's other alerts on one incident; the alert's summary still names the resource.

Application Signals and Container Insights on EKS

Application Signals services and Container Insights pods are not inventory resources. Their series belong to the EKS cluster they run in: an Application Signals environment eks:<cluster>/<namespace> names it, Container Insights' ClusterName dimension too. The cluster must be in the estate inventory (a tagged cluster is; Resource Explorer lists untagged ones) and inside the scope: its tags decide for every service and pod on it. Services of other environments (EC2, ECS, Lambda) are counted as series with no inventory resource. The alert's service is the series' own Service (R26, R27) or PodName (R30, the workload's name) dimension, and the cluster's name for R29, so a fault, a latency rise and a restarting pod of one service land on one incident. One resource carries at most 100 series a poll, shared round-robin across the rules, the rule with the fewest series first, so a cluster's many services never crowd out its one failed-node series (R29); the series past that are counted as over the 100 series per resource cap.

These rules follow the cluster's tags: on a cluster in scope, every Application Signals service and every pod is watched, whichever team's namespace it runs in. To keep to your own namespaces on a shared cluster, turn R26, R27 and R30 off and use the Kubernetes rules below, which read the namespace's labels.

R30 counts restarts in each 5-minute period (the CloudWatch agent reports the restarts since its previous collection): kubelet's crash-loop back-off caps at 5 minutes, so a container that keeps crashing restarts at least once in every period, and 3 periods in a row is 15 minutes of it.

Kubernetes state (R31 to R34)

A cluster's own state says what its metrics may not: a pod crash-looping, killed out of memory, stuck Pending, a node gone NotReady. Every poll, built-in detection reads the cluster through the workspace's read-only kubectl connection (kubectl get pods, get nodes, and get namespaces when a scope needs their labels — read verbs only, the same reader as the estate inventory, its namespace-by-namespace fallback included). This works for any cluster the connection reaches, EKS or not, and needs no Container Insights.

Scheduled maintenance (R35, R36)

AWS announces maintenance on a resource days or weeks ahead: an instance on degraded hardware is retired, a host is rebooted, a database gets an operating-system update. Nothing has failed yet, so these open a P4 incident on the resource itself (severity info: held, no paid analysis), in time to plan the change instead of meeting it. A mission-critical resource or a blast radius of 2 or more services raises the priority, as for any incident. P4 is our proposal: whether scheduled maintenance should be an incident at all, or a notice only, is still an open question. Once an hour, for each account and region with an in-scope EC2 instance or RDS database in the inventory, the evaluating AWS connection reads:

These are the EC2 and RDS APIs rather than the AWS Health API: they carry the same scheduled events for every account (the Health API needs a Business, Enterprise On-Ramp or Enterprise support plan) and name the resource directly. Only resources in the estate inventory and inside the scope are matched. One clean read ends an episode (the event completed or was canceled). A denied read keeps its rule's states and the card's Other reads line names the permission.

Provider health (R37)

When AWS itself has a problem, your alarms fire on the symptoms; R37 names the cause. Every poll, for each AWS account with an evaluating connection, built-in detection reads AWS's open issues for the services and regions the account's in-scope estate uses: the AWS Health API (health:DescribeEvents, on the global endpoint) through the connection, or, on an account without a Business, Enterprise On-Ramp or Enterprise support plan or a role without that permission, the public AWS Health Dashboard, which is not account-specific (the card's Other reads line then says needs health:DescribeEvents, and the Health API is asked again an hour later). An issue becomes an alert only where the estate is exposed: an in-scope resource of that service in that region (any service for an issue AWS files under multiple services).

New rules start in shadow

A workspace that was already armed when a release added rules (R26 to R37 in 0.1.153) does not start them firing unseen. Each new rule watches in shadow from the first poll it covers something, for the workspace's shadow period (24 hours by default): what it would have fired is recorded under Would have fired (shadow), the rule table says New rule: shadow until and when, and the card names the rules. When its period ends the rule arms itself; what it already had in alarm then opens with no analysis ("in alarm before built-in detection armed"), as at a workspace's own arming. Arm now arms them at the next poll with the usual severity rules. A workspace that turns detection on after the upgrade arms every rule together.

What it reads, and what it never does

Permissions

Read Why Where it is granted
cloudwatch:ListMetrics, cloudwatch:GetMetricData the rules ReadOnlyAccess and CloudWatchReadOnlyAccess; the CloudShell read role (its own region); the platform's inline read policy names them
elasticfilesystem:DescribeFileSystems, es:ListDomainNames, es:DescribeDomains, dynamodb:ListTables, sqs:ListQueues, sqs:GetQueueAttributes, ec2:DescribeNatGateways the inventory list reads behind the thresholds ReadOnlyAccess (not CloudWatchReadOnlyAccess, which grants none of them); the CloudShell read role (its own region); the platform's inline read policy names them
health:DescribeEvents provider health (R37); without a Business, Enterprise On-Ramp or Enterprise support plan the public AWS Health Dashboard instead ReadOnlyAccess; the CloudShell read role since 0.1.144 (on the global endpoint, signed in us-east-1)
ec2:DescribeInstanceStatus, rds:DescribePendingMaintenanceActions scheduled maintenance (R35, R36) ReadOnlyAccess; the CloudShell read role (its own region: rds:DescribePendingMaintenanceActions since 0.1.153, re-run the setup for an older role); the platform's inline read policy names them
apigateway:GET on arn:aws:apigateway:<region>::/restapis and …::/apis REST and HTTP API names the CloudShell read role grants these two collections only. The platform's Terraform read role carries ReadOnlyAccess, which grants apigateway:GET on every path, API key values included, so its inline policy denies the key paths (/apikeys, a usage plan's /keys); the agent also pauses for approval before it reads them, whatever role you supplied

The CloudShell read role lists Lambda functions (lambda:ListFunctions, since 0.1.146). A page carries each function's environment variables: values under a secret-looking name (DB_PASSWORD, API_TOKEN) are masked before anyone or any model sees the result, the others are kept.

A missing permission is never a silent zero. The connection's line on Datasources and the card say needs cloudwatch:GetMetricData (or ListMetrics) for the account and region when AWS answers access denied. Refused credentials — deactivated or expired keys, an expired session, a clock out of step — read as the connection's credentials were refused (code) instead, and a role that cannot be assumed says so: there is no permission to add. Each is retried once an hour, and at once after a passing Test or a configuration change. When every always-published series of an account (running instances' CPU, for example) comes back empty, the card says evaluation returned no data instead of reporting the account healthy, and a listing cut at its page cap says listing truncated.

Which connection evaluates which account

Each (account, region) is evaluated once, through one connection: the account's inventory connection when its regions cover that region, otherwise the first connection by name that runs Built-in detection and covers it. A CloudShell read role is pinned to one region, so several connections into one account usually split the regions between them. The job is one of the Jobs this connection runs on the connection (config key builtin_detection; only an explicit false opts a connection out). Several workspaces on one shared account evaluate, and pay, separately; a workspace with workspace scope tags evaluates only the resources carrying all of them (an untagged resource is outside the scope), a connection with Only findings on resources tagged only the resources it harvested that carry that tag (exact value), and either never shows the "no inventory resource" count, which would reveal other applications' resources.

Cost

GetMetricData costs US$ 0.01 per 1,000 metrics requested, billed to the AWS account the connection reads. The card counts the metrics each poll requests, the totals for today and yesterday, and the estimated month per account at the current poll and coverage. Example: 300 resources × 3 metrics, every 5 minutes, is about 7.8 million metrics a month, about US$ 78 — the same order as 900 standard alarms. ListMetrics calls (about one per metric per region per hour) fall inside CloudWatch's free API requests for most accounts.

Max resources per poll (300 by default) caps the read; the cap is filled round-robin across kinds, so a fleet of instances never starves the databases and load balancers, and a resource with a rule in alarm is always read. What the cap leaves out is counted as not covered.

Noise controls

Shadow mode, and arming

Turning detection on starts a shadow period (24 hours by default): rules are evaluated and every pair that would have fired is recorded on the card with its observed value and, per rule, a hit total; nothing is opened. The shadow clock runs only while something is covered — with no inventory yet, a missing permission or every connection opted out it pauses, and the card says why.

When the period ends the workspace arms itself. Pairs already in alarm at that moment open their incidents with no analysis ("in alarm before built-in detection armed"), because nobody was watching: a critical one still files its ticket and sends the open notification (press Analyse to start the analysis), a warning stays held like any warning. New episodes after that follow the usual severity rules. The owner can Arm now (the confirmation says how many incidents open and how many start an analysis; what is already in alarm then follows the usual rules), Keep shadow indefinitely, or switch single rules off in the rule table. Arm now takes effect at the next poll — the card says it is waiting until then — and is not offered while Keep shadow is on (untick it first); ticking Keep shadow cancels an Arm now still waiting.

Turning it on and off

What an alert looks like, and how it sits beside your own alarms

Each alert carries alertname = rule ID and name (R11 ALB 5xx rate), service = the resource's name in the inventory, resource and resource_id = its ARN, resource_type, rule, severity, cloud=aws, account, region, metric_namespace, and the CloudWatch dimensions as labels (LoadBalancer=app/web-alb/…), the way the CloudWatch alarm webhook labels an alarm. The incident title reads Built-in rule R11 · ALB 5xx rate > 5 % for 5 of 5 min · web-alb; the alert says what it observed against the threshold and links the rule's source. Target-group rules key on the target group itself, so one load-balancer outage can open an ALB incident and a target-group incident.

Beside a native CloudWatch alarm on the same resource: the alarm webhook names the resource by its dimension value, a built-in alert by its name in the inventory. They agree for an EC2 instance, an RDS instance and a Lambda function (the inventory names those by their ID or name), and for an SQS queue or DynamoDB table without a Name tag: the two alerts land on one incident. An EFS file system, or a queue or table with a Name tag, is named after that tag in the inventory (shared-data against fs-0abc…); load balancers, target groups, OpenSearch domains and API Gateway stages are named differently too (web-alb against app/web-alb/<id>). Those open separate incidents unless the alarm's description declares service=<name>. Both carry the dimension labels, so a label_map, criticality_map or assignment_map entry can target either. The source builtin is the platform's own: a webhook sender that names it is recorded as generic.

Limits

Next steps