{"id":"IL-0317","org":"Clerk","date":"2026-02-19","docs":[],"title":{"en":"Clerk's Postgres slowed after an automatic analyze flipped a query plan, and over 95% of requests got 429","ko":"Postgres 쿼리 플랜이 바뀌어 데이터베이스가 느려지고 Clerk 트래픽의 95% 넘게 429 를 받았다"},"domain":["database"],"impact":{"scope":"Over 95% of Clerk traffic returned 429 during an outage of about 90 minutes; sign in and account management APIs were unavailable; session tokens were served by a failover from 17:10 UTC","users":"Clerk customers and the users of their applications","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Clerk says a query plan flip can cause sudden, severe degradation and needs to be detected directly instead of through downstream symptoms; relying on those symptoms led the team to blame a customer's traffic pattern. Customers also told Clerk that status page updates came too rarely, the first update came too late and the initial severity label did not match the impact.","ko":"Clerk 는 쿼리 플랜이 바뀌는 문제가 갑자기 심하게 성능을 떨어뜨리므로, 뒤따르는 증상에 기대지 말고 바로 잡아내야 한다고 보았다. 이런 알림이 있었다면 고객의 트래픽 패턴이 원인이라고 잘못 보는 일을 피할 수 있었다. 데이터베이스가 내려가지 않고 느려지기만 하자 Postgres 장애를 기준으로 만든 Session API 의 자동 장애 조치는 작동하지 않았다. 고객들은 상태 페이지 공지가 너무 드물고, 첫 심각도 표시가 실제 영향과 맞지 않았으며, 첫 공지까지 시간이 너무 걸렸다고 지적했다."},"related":[],"sources":[{"url":"https://clerk.com/blog/2026-02-19-system-outage-postmortem","kind":"blog","archived":"https://web.archive.org/web/20261010193212/https://clerk.com/blog/2026-02-19-system-outage-postmortem"}],"summary":{"en":"The Postgres query planner chooses how to run a query from statistics that it collects from a sample of rows, and it re-analyzes tables regularly as data is added. In one column of the affected table 99.9996% of values were NULL. When the automatic analyze ran, its sample was too small and found only NULLs, so the planner concluded that 100% were NULL and planned the query as if one part would return no non-null rows, when it actually returned over 17,000. The query runs often enough that traversing those 17,000 rows each time took nearly all database resources, request handlers queued, and most requests were shed with status 429.\n\nThe team first suspected a customer's traffic spike and blocked it without effect. At 17:08 UTC a new failover that generates session tokens outside the core Session API was enabled, reducing backend load; it had been built and tested over the previous months but had no production triggers yet, and from 17:10 many users could reach customer applications again. At 17:25 the plan flip was identified, and at 17:27 ANALYZE was re-run manually on the table, the query returned to its earlier plan and the database began to recover. Systems were stable at 17:43, when the failover was disabled, and stable performance across the system was confirmed at 18:06. Right after the incident Clerk increased the statistics target on the table, and that evening refactored the query so the planner could plan it deterministically.","ko":"Postgres 에서는 쿼리를 실행하기 전에 쿼리 플래너가 데이터를 찾을 방법을 정하고, 데이터가 늘면 주기적으로 다시 분석해 플랜을 맞춘다. 플래너는 모든 행을 다시 읽지 않으려고 표본으로 데이터 분포를 어림한다. 문제의 쿼리에서 플래너는 한 컬럼의 NULL 비율에 기대고 있었는데, 실제 비율은 99.9996% 였다. 자동 ANALYZE 가 돌 때 표본이 너무 작아 NULL 만 뽑혔다. 플래너는 값이 100% NULL 이라고 판단했고, 쿼리의 그 부분이 NULL 이 아닌 행을 하나도 내지 않는다고 보고 플랜을 세웠다. 실제로는 17,000개가 넘는 행이 나왔고, 자주 실행되는 이 쿼리가 데이터베이스 자원 거의 전부를 차지했다. 요청 핸들러에 요청이 쌓여 트래픽의 95% 넘게 429 로 버려졌다. 장애는 2026-02-19 8:11 AM PST 에 시작됐다.\n\n16:15 UTC 에 사고가 내부에 보고되고 에스컬레이션됐다. 팀은 16:32 UTC 에 트래픽이 치솟은 고객을 원인으로 보고 16:35 UTC 에 그 트래픽을 막았지만 소용없었다. 16:50 UTC 에는 그 급증이 Clerk 인프라 안에서 난 실패를 재시도한 것이었음을 알았다. 16:55 UTC 에는 데이터베이스가 느려졌을 뿐 살아 있었기 때문에 Session API 의 자동 장애 조치가 작동하지 않았다는 것을 알았다. 17:08 UTC 에 팀은 새 장애 조치를 켜서 핵심 Session API 밖에서 세션 토큰을 만들게 했다. 이 장애 조치는 몇 달 동안 만들고 시험했지만 운영에서는 아직 작동 조건을 걸지 않은 상태였다. 17:10 UTC 에 많은 사용자가 고객 앱에 다시 들어갔지만 로그인과 계정 관리 API 는 여전히 쓸 수 없었다.\n\n17:25 UTC 에 자동 ANALYZE 로 바뀐 비효율적인 쿼리 플랜을 근본 원인으로 찾았다. 17:27 UTC 에 해당 테이블에 ANALYZE 를 수동으로 다시 돌리자 쿼리가 이전 플랜으로 돌아가 데이터베이스가 회복하기 시작했다. 17:43 UTC 에 시스템이 안정되고 데이터베이스가 정상 수준으로 돌아와 새 장애 조치를 껐다. 18:06 UTC 에는 전체 시스템이 안정된 것을 확인하고 모니터링 상태로 넘겼다. 서비스는 약 90분 만에 돌아왔다. 사고 직후 해당 테이블의 statistics target 을 올렸고, 그날 저녁 플래너가 결정론적인 플랜을 세울 수 있게 쿼리를 다시 짰다."},"trigger":{"en":"A routine automatic ANALYZE on one table sampled only NULL values in a column, and the Postgres planner switched a frequently run query to an inefficient plan.","ko":"Postgres 의 일상적인 자동 ANALYZE 가 표본을 너무 적게 뽑아 한 컬럼의 값이 100% NULL 이라고 잘못 판단했다. 그 결과 자주 실행되는 쿼리의 플랜이 비효율적인 것으로 바뀌었다."},"severity":"unknown","symptoms":["service-unavailable","database-overloaded","high-latency"],"timeline":{"detected_after":"4m","resolved_after":"1h 30m"},"date_kind":"incident","detection":{"en":"The incident was reported and escalated internally at 16:15 UTC, and the team started a group call.","ko":"16:15 UTC 에 사고가 내부에 보고되고 에스컬레이션됐고, 팀이 그룹 통화를 열어 근본 원인을 찾기 시작했다."},"components":["database","query-planner"],"root_cause":"query-plan-regression","remediation":[{"text":{"en":"Enabled a new failover that generates session tokens outside the core Session API","ko":"17:08 UTC 에 핵심 Session API 밖에서 세션 토큰을 만드는 새 장애 조치를 켜서 백엔드 부하를 줄였고, 17:10 UTC 에 많은 사용자가 고객 앱에 다시 들어갔다."}},{"text":{"en":"Re-ran ANALYZE manually on the affected table, which put the query back on its earlier plan","ko":"17:27 UTC 에 해당 테이블에 ANALYZE 를 수동으로 다시 돌렸고, 쿼리가 이전 플랜으로 돌아가 데이터베이스가 회복하기 시작했다."}},{"text":{"en":"Disabled the failover once the database was back to healthy levels","ko":"17:43 UTC 에 시스템이 안정되고 데이터베이스가 정상 수준으로 돌아오자 새 장애 조치를 껐다."}},{"text":{"en":"Increased the statistics target on the table so the planner samples more rows","ko":"사고 직후 해당 테이블의 statistics target 을 올려 쿼리 플래너가 더 크고 정확한 표본을 보게 했다."}},{"text":{"en":"Refactored the query so the planner could plan it deterministically","ko":"그날 저녁 플래너가 결정론적인 플랜을 세울 수 있게 쿼리를 다시 짰다."}}],"action_items":[{"text":{"en":"Add dedicated alerting for database query plan flips","ko":"데이터베이스 쿼리 플랜이 바뀌는 것을 잡는 전용 알림을 더한다."}},{"text":{"en":"Train the reliability team to enable the new session token failover and add automatic triggers for it","ko":"안정성 팀이 새 세션 토큰 장애 조치를 켜는 방법을 익히게 하고, 자동 작동 조건을 건다. 몇 주 안에 끝내는 것이 목표다."}},{"text":{"en":"Audit all queries for others at risk of inefficient plans from unstable statistics","ko":"불안정한 통계로 비효율적인 플랜이 생길 위험이 있는 쿼리가 더 있는지 모든 쿼리를 점검한다."}},{"text":{"en":"Name a dedicated communications lead during incidents","ko":"사고 때 소통 전담자를 둔다."}},{"text":{"en":"Post status page updates at a regular cadence even when nothing has changed","ko":"상황이 그대로여도 정해진 간격으로 상태 페이지를 갱신한다."}},{"text":{"en":"Make status page severity labels match the impact on customers","ko":"상태 페이지의 심각도 표시가 고객이 겪는 영향과 맞게 한다."}},{"text":{"en":"Cross-post incident updates to social channels","ko":"사고 도구를 고쳐 공지가 소셜 채널에도 함께 올라가게 한다."}}],"contributing":["inefficient-query","monitoring-blind-spot"],"recurrence_of":null,"symptoms_text":{"en":"At 8:11 AM PST (16:11 UTC) on 19 February 2026, Clerk's database performance dropped at once. Slow database queries made request handlers queue, and over 95% of traffic was answered with 429 without being handled; the small share of requests that reached the database returned 200, but extremely slowly.\n\nThe incident was reported and escalated internally at 16:15 UTC. At 16:32 the team found a customer whose traffic spiked at the same time as the outage and suspected that load had triggered the database problems. Blocking that customer's traffic at 16:35 did not help, and by 16:50 the team found that the customer's overly aggressive retry mechanism had caused the spike, retrying failures that started inside Clerk's infrastructure.\n\nThe automatic failover for Clerk's Session API did not trigger, because the database was still online, only degraded. Even after a newer failover for session tokens was enabled at 17:08, sign in and account management APIs stayed unavailable.","ko":"2026-02-19 8:11 AM PST 에 데이터베이스 성능이 갑자기 떨어졌다. 요청 핸들러에 요청이 쌓였고, Clerk 트래픽의 95% 넘게 처리되지 못한 채 429 를 받았다. 데이터베이스까지 간 일부 요청은 200 을 받았지만 아주 느렸다.\n\n문제의 쿼리는 자주 실행되는 쿼리였다. 이 쿼리가 예상치 못하게 17,000개가 넘는 행을 훑으면서 데이터베이스 자원 거의 전부를 차지했다.\n\n17:10 UTC 에 세션 토큰 장애 조치가 성공한 뒤로 많은 사용자가 고객 앱에 다시 들어갈 수 있었지만, 로그인과 계정 관리 API 는 여전히 쓸 수 없었다.\n\n16:32 UTC 에 팀은 장애와 같은 때에 트래픽이 비정상으로 치솟은 고객 하나를 찾았고, 그 부하가 데이터베이스 문제를 일으켰다고 보았다. 16:35 UTC 에 이 고객의 트래픽을 수동으로 막았지만 시스템은 돌아오지 않았다. 16:50 UTC 에 팀은 그 고객의 급증이 지나치게 공격적인 재시도 때문이었고, 재시도를 부른 첫 실패는 Clerk 인프라 안에 있었다는 것을 알았다.\n\nSession API 의 자동 장애 조치는 작동하지 않았다. 데이터베이스가 느려졌을 뿐 엄밀히는 살아 있었기 때문이다."},"updated_at":"2026-11-14T12:33:57.085257Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0317","org_slug":"clerk"}