{"id":"IL-0497","org":"Buttondown","date":"2026-03-31","docs":[],"title":{"en":"Buttondown's database ran out of connections twice in one day and requests failed while health checks mostly passed","ko":"Postgres 가 설정된 최대 연결 수에 닿아 하루에 두 번 새 요청이 연결을 얻지 못하고 실패했다"},"domain":["database"],"impact":{"scope":"Buttondown's service: requests that needed a database connection failed, in two outages of about seven and about thirteen minutes","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Buttondown notes that the tool used to recover from the first outage needed the same resource that was exhausted, which locked the team out during the second outage, and that a health check which cannot detect connection exhaustion, the most common failure mode, gives little protection. A configuration value set long ago drifted out of line with traffic because nobody reviewed it.","ko":"관리용 연결을 하나라도 예약해 뒀다면 두 번째 장애에서도 첫 번째처럼 쿼리를 죽일 수 있었다. 두 번째 장애가 첫 번째보다 거의 두 배 길었던 것은 복구에 쓰던 수단 자체를 쓸 수 없었기 때문이었다. 가장 흔한 장애를 잡지 못하는 헬스 체크는 헬스 체크 구실을 하지 못한다. 오래전에 정하고 다시 보지 않은 연결 상한은 트래픽이 바뀌면서 서서히 모자라게 됐다."},"related":[],"sources":[{"url":"https://buttondown.com/blog/incident-0024","kind":"blog","archived":"https://web.archive.org/web/20261029083559/https://buttondown.com/blog/incident-0024"}],"summary":{"en":"On 31 March 2026 Buttondown's database reached the maximum number of connections it was configured to accept, and once that ceiling was hit, new requests could not acquire a connection and failed. The database itself stayed healthy. The connection limit had been set a long time before and had not been revisited as Buttondown's traffic patterns changed. The health check endpoint returned a static 200 without checking that the process could get a database connection, and the external checker's requests reached workers that already held connections, so monitoring did not alert and the outage was found through user reports and manual observation.\n\nIn the first outage the team identified the connection exhaustion and killed active queries to free connections, with about seven minutes of downtime. The same root cause came back later that day, and this time no connection was free to kill queries with, so the database had to be restarted to force-kill the running queries; that outage lasted about thirteen minutes. Buttondown says the second outage lasted nearly twice as long because no connection was reserved for administrative access.","ko":"2026년 3월 31일 Buttondown 은 약 7분과 약 13분, 두 번 멈췄다. 두 번 모두 원인은 같았다. Postgres 가 받도록 설정된 연결 수의 상한에 닿았고, 그 뒤 새 요청은 연결을 얻지 못하고 실패했다. 쿼리 속도, CPU, 메모리, 복제 지연은 모두 정상이었다. 이 상한은 오래전에 정한 뒤 트래픽 패턴이 바뀌는 동안 한 번도 다시 보지 않은 값이었다.\n\n헬스 체크 엔드포인트는 데이터베이스 연결을 확인하지 않고 늘 200 으로 응답했고, 헬스 체커의 요청은 이미 연결을 가진 워커에 닿아 대부분 성공했다. 그래서 자동 알림은 울리지 않았고, 팀은 사용자 신고와 직접 관찰로 장애를 알았다.\n\n첫 번째 장애에서 팀은 연결 고갈을 확인하고 실행 중인 쿼리를 죽여 연결을 풀었고, 장애는 약 7분 만에 끝났다. 두 번째 장애에서는 연결이 완전히 차서 쿼리를 죽이려고 접속할 수도 없었다. 팀은 실행 중인 쿼리를 강제로 끝내려고 데이터베이스를 재시작했고, 장애는 약 13분 이어졌다."},"trigger":null,"severity":"unknown","symptoms":["connection-pool-exhausted","service-unavailable"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"incident","detection":{"en":"User reports and the team's own manual observation, not automated monitoring. The health check endpoint kept returning 200 for most requests, so no alert fired.","ko":"팀은 자동 시스템이 아니라 사용자 신고와 직접 관찰로 장애를 알았다. 헬스 체커는 대체로 정상으로 보고했고 외부 모니터링도 알림을 내지 않았다."},"components":["database"],"root_cause":"capacity","remediation":[{"text":{"en":"Killed active queries to free database connections in the first outage","ko":"첫 번째 장애에서 연결 고갈을 확인하고 실행 중인 쿼리를 죽여 연결을 풀었다."}},{"text":{"en":"Restarted the database to force-kill the running queries in the second outage","ko":"두 번째 장애에서는 쿼리를 죽이려고 접속할 연결조차 없어 데이터베이스를 재시작해 실행 중인 쿼리를 강제로 끝냈다."}}],"action_items":[{"text":{"en":"Reserve a small number of database connections for administrative access","ko":"Postgres 에서 관리용 접속에만 쓰는 연결을 몇 개 예약해 둔다."}},{"text":{"en":"Monitor and alert on the database connection count as a percentage of the configured maximum","ko":"설정된 최대치 대비 연결 수의 비율을 데이터베이스에서 직접 감시하고 상한에 가까워지면 알린다. 이 감시는 HTTP 요청의 성공 여부와 상관없이 데이터베이스 자체를 본다."}},{"text":{"en":"Make the health check endpoint run a lightweight query so connection exhaustion shows in the health checker and the load balancer","ko":"늘 200 으로 응답하던 헬스 체크 엔드포인트가 가벼운 쿼리를 실행해 보도록 바꿔, 연결 고갈이 헬스 체커와 로드 밸런서에 바로 보이게 한다."}},{"text":{"en":"Add database connection limits to the quarterly capacity review","ko":"분기별 용량 검토에 연결 상한을 넣는다."}},{"text":{"en":"Document and script a fallback that force-recycles workers to release connections without needing a database connection","ko":"관리용 접속까지 막혔을 때를 대비해, 데이터베이스 연결 없이 워커를 강제로 재활용해 연결을 푸는 대체 절차를 문서와 스크립트로 만든다."}}],"contributing":["no-capacity-planning","monitoring-blind-spot","circular-dependency"],"recurrence_of":null,"symptoms_text":{"en":"On 31 March 2026 Buttondown was down twice, for about seven and about thirteen minutes. New requests could not get a database connection and failed, although the database itself was healthy: queries were fast, CPU and memory were fine and replication lag was nominal.\n\nNo automated alert fired. The health check endpoint returned 200 for most requests, because the health checker's requests happened to land on workers that already held open database connections. The team found out from user reports and its own manual observation.\n\nIn the first outage the team killed active queries to free connections. In the second outage the connection count was so saturated that the team could not connect to the database to kill queries, because the tool used in the first outage itself needed a database connection.","ko":"2026년 3월 31일 Buttondown 이 두 번 멈췄다. 약 7분과 약 13분 동안 새 요청이 데이터베이스 연결을 얻지 못해 실패했고, 팀은 자동 알림이 아니라 사용자 신고와 직접 관찰로 장애를 알았다.\n\n데이터베이스 자체에는 문제가 없었다. 쿼리는 빨랐고 CPU 와 메모리도 문제가 없었으며 복제 지연도 정상 범위였다. 문제는 데이터베이스가 받도록 설정된 연결 수의 상한에 닿은 것이었다.\n\n헬스 체커는 대체로 정상으로 보고했다. 헬스 체크 엔드포인트는 데이터베이스 연결을 확인하지 않고 늘 200 으로 응답했고, 헬스 체커의 요청은 마침 연결을 이미 쥐고 있던 워커에 닿아 대부분 성공했다. 외부 모니터링의 요청도 알림이 울리지 않을 만큼은 통과했다.\n\n두 번째 장애 때는 연결이 완전히 차서 쿼리를 죽이려고 데이터베이스에 접속하는 것조차 할 수 없었다. 첫 번째 장애를 해결한 도구를 쓰려면 바로 그 고갈된 연결이 필요했다."},"updated_at":"2026-11-14T12:34:14.206276Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0497","org_slug":"buttondown"}