{"id":"IL-0315","org":"Railway","date":"2025-10-28","docs":[],"title":{"en":"Railway's backend API went down when an index build locked a billion-row Postgres table and used up every connection","ko":"약 10억 행 테이블에 인덱스를 만드는 스키마 변경이 테이블 전체를 잠가 Railway 의 백엔드 API 가 멈췄다"},"domain":["database"],"impact":{"scope":"Dashboard inaccessible, CLI operations that use the API failed, GitHub-based deployments delayed and all Public API operations disrupted; running deployments and platform-level features stayed online","users":"Users who opened the dashboard, used the CLI or started a GitHub-based deploy during the incident","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":null,"related":[],"sources":[{"url":"https://blog.railway.com/p/incident-report-oct-28th-2025","kind":"blog","archived":"https://web.archive.org/web/20261023024713/https://blog.railway.com/p/incident-report-oct-28th-2025"}],"summary":{"en":"Railway's Dashboard, CLI and Public API depend on a backend that uses Postgres as its primary datastore. A routine change added a new column with an index to a table of about 1 billion records that nearly all API operations use. The index was created without Postgres' CONCURRENTLY option, so it took an exclusive lock on the whole table, and every query queued behind it. The API servers kept accepting requests and each new database connection blocked, and Railway's PgBouncer configuration allowed more connections than the Postgres limit, so all connection slots, including the administrative ones, were exhausted.\n\nEngineers could not open an administrative connection to stop the index creation. While other recovery options were being explored, the migration finished on its own after about 30 minutes and released the table lock, the queued operations ran and the connection pools returned to normal. The root cause was identified at 18:56 UTC, gradual recovery was confirmed at 19:00 and full service at 19:15. Running deployments and platform-level features stayed online throughout.","ko":"Railway 의 대시보드, CLI, Public API 같은 클라이언트용 인터페이스는 백엔드에 기대고, 백엔드는 Postgres 를 주 데이터 저장소로 쓴다. 2025-10-28 18:23 UTC 에 스키마 변경을 담은 데이터베이스 변경이 들어갔고, 18:34 UTC 에 운영에 반영됐다. 이 변경은 거의 모든 API 작업이 쓰는 약 10억 행 테이블에 새 컬럼과 인덱스를 더했다. 인덱스를 CONCURRENTLY 옵션 없이 만들었기 때문에 테이블 전체에 배타적 잠금이 걸렸다. 잠금이 걸린 동안 데이터베이스로 가는 모든 쿼리가 인덱스 작업 뒤에 줄을 섰고, API 서버는 요청을 계속 받아 연결을 열 때마다 곧바로 막혔다. PgBouncer 설정이 Postgres 의 커넥션 한도를 넘는 연결을 허용해, 관리용 슬롯까지 모두 찼다.\n\n18:36 UTC 에 내부 모니터링 알림이 울렸고 18:41 UTC 에 사고를 선언했다. 엔지니어는 인덱스 생성을 끝내려 했지만 FATAL: remaining connection slots are reserved for roles with privileges of the \"pg_use_reserved_connections\" role 오류로 관리용 연결을 맺지 못했다. 18:56 UTC 에 근본 원인을 찾았다. 다른 복구 방법을 찾는 동안 마이그레이션이 약 30분 만에 스스로 끝나 테이블 잠금이 풀렸고, 밀린 작업이 처리되며 커넥션 풀이 정상으로 돌아왔다. 19:00 UTC 에 서비스가 차츰 돌아오는 것을 확인했고 19:15 UTC 에 완전 복구를 확인했다."},"trigger":{"en":"A routine schema change to the backend's Postgres database, introduced at 18:23 UTC and live in production at 18:34 UTC, that added a new column with an index without the CONCURRENTLY option.","ko":"일상적인 Postgres 데이터베이스 변경으로 약 10억 행 테이블에 새 컬럼과 인덱스가 더해졌다. 인덱스 생성에 CONCURRENTLY 옵션을 쓰지 않아 테이블 전체에 배타적 잠금이 걸렸다."},"severity":"unknown","symptoms":["service-unavailable","lock-contention","database-overloaded"],"timeline":{"detected_after":"2m","resolved_after":"41m"},"date_kind":"incident","detection":{"en":"Internal monitoring alerts paged the engineering team at 18:36 UTC, and the incident was declared at 18:41 UTC.","ko":"변경이 운영에 반영되고 2분 뒤인 18:36 UTC 에 내부 모니터링 알림이 울려 엔지니어링 팀이 호출됐다."},"components":["database","database-proxy"],"root_cause":"deploy-rollout","remediation":[{"text":{"en":"The index migration finished after about 30 minutes and released the table lock, and the queued operations and connection pools recovered","ko":"인덱스를 만드는 마이그레이션이 약 30분 만에 끝나 테이블 잠금이 스스로 풀렸고, 줄을 선 작업이 처리되며 커넥션 풀이 정상으로 돌아왔다."}}],"action_items":[{"text":{"en":"Enforce CONCURRENTLY for all index creation in CI and block pull requests that do not use it","ko":"CI 에서 모든 인덱스 생성에 CONCURRENTLY 사용을 강제하고, 지키지 않은 풀 리퀘스트는 머지 전에 막는다."}},{"text":{"en":"Adjust PgBouncer pool limits so they cannot overwhelm the Postgres instance's capacity","ko":"PgBouncer 커넥션 풀 한도를 조정해 뒤에 있는 Postgres 인스턴스의 용량을 넘지 않게 한다."}},{"text":{"en":"Configure database user connection limits so administrative access stays possible during incidents","ko":"데이터베이스 사용자별 커넥션 한도를 설정해 장애 중에도 관리용 접속을 보장하고, 어떤 상황에서도 유지 보수 작업을 할 수 있게 한다."}}],"contributing":["no-config-tests","limited-debug-access"],"recurrence_of":null,"symptoms_text":{"en":"At 18:36 UTC on 28 October 2025, two minutes after a database change went live in production, Railway's internal monitoring alerted and paged the engineering team. The dashboard could not be opened, CLI operations that use the API, such as railway up, failed, GitHub-based deployments were delayed because webhook processing failed, and all Railway Public API operations were disrupted.\n\nThe change added a new column with an index to a table of about 1 billion records that nearly all API operations use. All queries against the database queued behind the index operation, and the API servers kept accepting requests whose database connections blocked on the locked table straight away. The connections that PgBouncer opened went past the Postgres connection limit and took every connection slot, the administrative ones included.\n\nWhen engineers tried to connect and stop the index creation, Postgres refused with \"FATAL: remaining connection slots are reserved for roles with privileges of the \"pg_use_reserved_connections\" role\". Without an administrative connection, no command could be run to terminate the operation.","ko":"2025-10-28 18:36 UTC 에 내부 모니터링 알림이 울려 엔지니어링 팀이 호출됐다. 대시보드가 열리지 않았고, railway up 처럼 API 를 쓰는 CLI 작업이 실패했으며, Public API 작업이 모두 멈췄다.\n\n웹훅 처리가 실패해 GitHub 기반 배포가 늦어졌다. 이미 돌고 있던 배포와 플랫폼 수준의 기능은 내내 정상으로 동작했다.\n\n잠긴 테이블 뒤로 모든 쿼리가 줄을 서는 동안 API 서버는 요청을 계속 받았고, 요청마다 연 데이터베이스 연결이 곧바로 막혔다. PgBouncer 설정이 Postgres 의 커넥션 한도를 넘는 연결을 허용해, 관리용 슬롯까지 모든 연결 슬롯이 찼다.\n\n엔지니어가 인덱스 생성을 멈추려고 접속하자 다음 오류가 났다.\nFATAL: remaining connection slots are reserved for roles with privileges of the \"pg_use_reserved_connections\" role\n관리용 연결을 맺을 수 없어 문제가 된 인덱스 생성을 끝내는 명령을 실행하지 못했다."},"updated_at":"2026-11-14T12:33:56.885471Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0315","org_slug":"railway"}