Incident Library

IL-0333 Buildkite · 2016-08-22databaseaws

A downsized RDS PostgreSQL database maxed out its CPU at US peak and failing ELB health checks took buildkite.com down

Buildkite had grown on AWS Activate credits by adding hardware. About two weeks before the incident the team found the credits were about to run out and set out to cut the bill quickly. The largest cost was an m4.10xlarge Multi-AZ RDS PostgreSQL database, which was downgraded during a weekend maintenance window to an r3.2xlarge Multi-AZ instance. Because the new instance had less memory, connection pooling was moved from pgbouncer on each host to one centralised pgbouncer. Neither change was load tested. The database looked fine over the weekend and into what the team took for the start of the US peak, and the team went to bed.

At about 16:00 UTC the database CPU maxed out. Buildkite runs three ELBs with Auto Scaling Groups, for the dashboard, the REST API and the Agent API, and the health check behind them tested both SQL databases, Redis and Memcached. Health checks returned HTTP 500, the dashboard ELB removed its servers, and buildkite.com went offline at 17:21 UTC. Replacement servers deployed themselves from a baked AMI and looked up the current revision from buildkite.com itself; when that failed they ran the AMI's old code, whose health check referenced a decommissioned database, so they failed and were terminated in a loop. The PagerDuty alerts from New Relic, Pingdom, Datadog, Calibre and CloudWatch did not wake anyone: the on-call engineer's "Immediately phone me" notification rule had dropped out of his settings, so only silent push notifications arrived, and the other team members, reached by escalation, had their phones on silent.

The team woke at 21:00 UTC. An AWS IAM issue blocked the database upgrade and the launch of a fixed AMI in the AWS Console, so at 22:28 UTC servers were taken from the Agent API and added to the dashboard ELB, bringing buildkite.com back with slow responses. When IAM recovered, the EC2 request limit, used up by the server churn, forced the team to wait. The team switched the Auto Scaling Groups to EC2 health checks and locked their desired counts, then used the aws-cli to upgrade the database to an instance type that could handle the load and roll out servers whose self-deploy worked, and went back to pgbouncer on each host. Buildkite fully recovered at 23:10 UTC.

Severity
–
Scope
buildkite.com dashboard down: no login, no build logs, no documentation; builds kept running and pull request statuses kept updating; slow responses and dropped requests from 22:28 to 23:10 UTC
Users affected
–
Region
–
Time to detect
3h 39m
Time to resolve
5h 49m

Symptoms

On 22 August 2016 at 17:21 UTC buildkite.com went offline: nobody could log in, view build logs or read the documentation, although builds kept running and GitHub and Bitbucket pull request statuses kept updating. The Buildkite team in Melbourne was asleep and woke at 21:00 UTC, almost 4 hours later, to emails, tweets and Slack messages saying Buildkite was down.

At about 16:00 UTC, during US peak load, the RDS PostgreSQL database that had been downgraded over the weekend could no longer keep up and its CPU maxed out. The health check endpoint behind each Elastic Load Balancer ran SELECT 1 against both SQL databases and pinged Redis and Memcached, so it began returning HTTP 500 errors saying it could not connect to the database. The ELB in front of the dashboard had the lowest failed health check threshold and marked its servers OutOfService; the ELBs for the REST API and the Agent API had problems but kept their servers.

The Auto Scaling Group launched replacement servers, but every one of them failed its health check and was terminated, and this launch-and-terminate cycle went on for hours. New servers were built from an AMI with an old version of the code baked in and found the current Git revision by requesting https://buildkite.com/_secret/version, which could not answer while buildkite.com was down. The baked-in code's health check still referenced Job::Log::Chunk::Shard::One, a database that had been decommissioned.

Recovery was slowed by AWS. An IAM problem at AWS stopped the team from upgrading the database in the AWS Console, and when the team tried to launch a fixed AMI for testing, the IAM role selector did not work and the AWS Console kept showing "Internal server error". Once IAM recovered, new EC2 servers could not be launched because the launch-and-terminate cycle had used up the EC2 request limit. After a partial recovery at 22:28 UTC, responses stayed very slow and some requests were dropped.

Symptom Service outageDatabase overloadProbe failuresCloud API rate limit exhausted

Root cause

Trigger
Over a weekend maintenance window, Buildkite downgraded its m4.10xlarge Multi-AZ RDS PostgreSQL database to an r3.2xlarge Multi-AZ instance to cut its AWS bill, and moved to a centralised pgbouncer to pool connections. The first US weekday peak after the change pushed the new instance's CPU to its maximum at about 16:00 UTC.

Detection

New Relic, Pingdom, Datadog, Calibre and CloudWatch raised PagerDuty alerts, but the on-call phone call never came and only silent push notifications arrived. The team noticed at 21:00 UTC when they woke to emails, tweets and Slack messages from users.

Remediation

Action items

Lessons

Buildkite listed six lessons. Watch AWS credits, because billing reports show $0 while credits are spent, so the real cost is hidden and big changes end up rushed. Load test after significant infrastructure changes. Think about what a health check should do and what happens when every server behind an ELB fails it. Do not make new servers look up the deployed revision from the service they are replacing. Make sure every on-call member has the right PagerDuty and phone settings before going on call. When servers churn in AWS, stop health checks and autoscaling, for example with the Auto Scaling feature to suspend and resume processes, so the EC2 request limit is not used up.

Sources

References

Related cases

Check your cluster for this

kubegap audits a Kubernetes cluster for conditions like the ones in this case, Database, Cloud load balancer, Liveness and readiness probes among them, read-only, from Claude Code or Codex. Its checks cite public reports like this one.

For AI tools and scripts

The API needs no key. The MCP server offers search_incidents, get_incident, list_incidents, get_topic and vocab; both are explained on API and MCP.