{"id":"IL-0100","org":"PostHog","date":"2025-10-21","docs":[],"title":{"en":"PostHog's feature flags pods were packed onto Kubernetes nodes that were too small for their CPU requests, and CPU saturation above 90% caused three outages in ten days through Redis overload, connection pool exhaustion and slow queries","ko":"노드가 파드의 CPU 요청에 비해 작아 CPU 가 포화되면서 PostHog 기능 플래그 서비스에 열흘 사이 장애가 세 번 났다."},"domain":["kubernetes","aws"],"impact":{"scope":"PostHog feature flags evaluation, all flag types; the main PostHog application was also affected through the shared Redis","users":"About 38% of evaluation requests failed on 21 October and about 34% on 28 October in the US datacenter; about 97% of evaluation requests worldwide got 429 errors on 24 October; queries were slow for 7 hours 9 minutes on 29 to 30 October","region":"US datacenter on 21 and 28 October; worldwide on 24 October"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Nodes that are small relative to pod requests let the scheduler pack many pods on each node, and the resulting CPU saturation showed up three different ways: excessive parallelism, connection pool exhaustion and slow queries. Treating each symptom separately let the problem return; right-sizing the pods and nodes fixed all three.\n\nCPU must be alerted on from the start. Unbounded Envoy retries and unbounded cache population amplified the overload, and a Redis instance shared with the main application spread a flags problem to the main application. A rollback did not end the incident, because the data transfer and resource exhaustion it had started kept running.","ko":"가장 큰 교훈은 CPU 크기 산정이었다. 노드가 파드 요청에 비해 작으면 노드 하나에 파드가 너무 많이 몰리고, CPU 포화가 과도한 병렬 처리, 연결 풀 고갈, 느린 쿼리처럼 서로 다른 모습으로 나타난다. 증상만 고친 수정 때문에 21일의 장애가 28일에 되풀이됐다. PostHog 는 CPU 를 넉넉히 받은 파드를 적은 수로 돌리는 편이 낫다고 정리했다.\n\n기능 플래그 서비스와 본 애플리케이션이 Redis 를 같이 쓰면 한쪽 과부하가 다른 쪽을 쓰러뜨리는 단일 장애 지점이 된다. CPU 지표는 처음부터 경보로 걸어야 하고, Postgres 에서 Redis 로 가는 전송 급증도 경보 대상이다. 재시도, 캐시 채우기, 연결 생성에 한도가 없으면 장애가 커진다. 과부하 패턴은 운영 트래픽에서만 드러났으므로 부하 시험이 필요하고, 중요한 설정은 배포 없이 바꿀 수 있어야 한다."},"related":[],"sources":[{"url":"https://posthog.com/handbook/company/post-mortems/2025-10-21-feature-flags-recurring-outages","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"Between 21 and 30 October 2025 the PostHog feature flags service had four incidents with more than 14 hours of major impact in total. Three of them had the same root cause: the Kubernetes nodes were too small relative to the pods' resource requests, so Kubernetes packed too many flags pods onto each node and node CPU went above 90%. The fourth, on 24 October, was a separate rate limiting misconfiguration: the tower-governor middleware by default does not trust X-Forwarded-For, so it saw all traffic as coming from the load balancer's IP, and about 97% of requests got 429 errors for 72 minutes until rate limiting was disabled. The rate limiting had been tested only directly against the service, not behind the production load balancers.\n\nUnder that CPU pressure, degraded requests made Envoy retry between the load balancer and the service. Each retry created new concurrent requests, each request did several concurrent Redis reads, and on a cache miss the service synchronously loaded the full flag and team state from Postgres and wrote it into Redis. One slow request could therefore fan out into dozens of Redis operations, and the result was bursts of writes that overloaded Redis. At the same time every pod keeps its own Postgres connection pool, and building one needs TLS handshakes, authentication and connection setup, which is expensive on a CPU-bound pod. New pods could not finish this within the 20-second startup timeout and went into crash loops, which reduced healthy capacity further. Because the flags service shares its Redis instance with the main PostHog application, the Redis overload spread to the main app.\n\nOn 21 October the triggering deploy was rolled back within 2 minutes, but the data transfer and resource exhaustion continued; the service recovered at 23:28 UTC after connections and Redis writes were throttled (103 minutes). On 28 October the team stopped the rollout, scaled the US pods to zero until Redis recovered from the write storm, and brought them back gradually; full service returned at 21:31 UTC (123 minutes). On 29 to 30 October CPU saturation directly slowed queries for 7 hours 9 minutes. What fixed all three was increasing the pods' resource requests and adjusting the fleet size so fewer, better-resourced pods ran on each node, which brought per-node CPU down. PostHog also added CPU alerts, which had been missing throughout, limited Envoy retries, and configured tower-governor to trust X-Forwarded-For from its load balancer.","ko":"PostHog 기능 플래그 서비스는 2025-10-21 부터 10-30 까지 열흘 동안 장애를 네 번 겪었다. 21일 103분, 24일 72분, 28일 123분, 29일에서 30일 사이 7시간 9분이었고, 오류나 심한 지연이 이어진 시간을 합치면 14시간이 넘었다. 네 번 중 세 번은 원인이 같았다. 노드가 파드의 CPU 요청에 비해 작아서 쿠버네티스가 노드 하나에 파드를 너무 많이 넣었고, 노드 CPU 사용률이 90% 를 넘었다.\n\nCPU 가 포화되자 두 가지 일이 벌어졌다. 첫째, 요청이 느려지면 로드밸런서와 서비스 사이의 Envoy 가 재시도했다. 재시도할 때마다 새 요청이 생겼고, 요청마다 Redis 를 여러 번 동시에 읽었다. 느려진 요청 하나가 수십 개의 Redis 작업으로 불어났다. 캐시 미스가 나면 플래그와 팀 상태 전체를 Postgres 에서 동기로 읽어 Redis 에 썼기 때문에 Redis 에 쓰기가 한꺼번에 몰렸다. 둘째, 파드마다 Postgres 연결 풀을 따로 두는데, 풀을 만들 때 하는 TLS 핸드셰이크와 인증과 연결 수립은 CPU 를 많이 쓴다. CPU 가 90% 를 넘자 새 파드가 20초 시작 타임아웃 안에 풀을 만들지 못해 크래시 루프에 빠졌고, 정상 파드 수가 줄었다.\n\n21일에는 타임아웃 오류를 줄이려던 배포가 이 연쇄를 일으켰다. 2분 안에 롤백했지만 Postgres 에서 Redis 로 가는 대량 전송과 동시 연결 급증은 멈추지 않았고, Redis 메모리가 바닥났다. 팀은 23:00 UTC 부터 연결과 Redis 쓰기를 조절해 23:28 UTC 에 복구했다. 28일에는 일상 배포가 US 플래그 파드를 롤아웃시키면서 같은 일이 되풀이됐다. 팀은 롤아웃을 멈추고 US 파드를 0개로 줄여 Redis 가 안정되기를 기다린 뒤, 파드를 차례로 다시 올려 21:31 UTC 에 복구했다. 두 번 모두 Redis 를 같이 쓰는 PostHog 본 애플리케이션도 영향을 받았다.\n\n29일에서 30일 사이에는 CPU 포화가 곧바로 느린 쿼리로 나타났다. Redis 와 Postgres 는 정상이었지만 노드 CPU 압박 때문에 연결과 응답이 느려졌다. 하드웨어 변경을 롤백하자 오류는 대부분 가라앉았지만 지연은 남았다. 플래그 서비스의 파드 리소스 요청을 늘려 노드당 파드 수를 줄이자 노드 CPU 사용률이 내려가 서비스가 정상으로 돌아왔다.\n\n24일 장애는 원인이 달랐다. 21일 장애 뒤 보호 장치로 /flags 엔드포인트에 IP 기반 레이트 리밋을 넣었다. 그런데 이 레이트 리밋을 맡은 Rust 미들웨어 tower-governor 는 기본값으로 X-Forwarded-For 헤더를 믿지 않아서, 모든 트래픽을 로드밸런서 IP 하나에서 온 것으로 봤다. 레이트 리밋은 서비스에 직접 요청을 보내는 시험에서만 확인했고 운영 로드밸런서 뒤에서는 시험하지 않았다. 18:01 UTC 부터 대부분 요청이 429 를 받았고, 19:12 UTC 에 레이트 리밋을 끄자 복구됐다."},"trigger":{"en":"On 21 October a deployment meant to reduce timeout errors (PR #39821); on 24 October the deployment of IP-based rate limiting (PR #40074) that saw all traffic as coming from the load balancer's IP; on 28 October an unrelated routine deployment that rolled the feature flag pods in the US region.","ko":"10월 21일에는 증상만 고치려던 타임아웃 처리 배포가 장애를 일으켰다. 28일에는 플래그 서비스와 무관한 일상 배포가 파드 롤아웃을 일으켰고, 이 롤아웃이 장애로 이어졌다. 24일에는 로드밸런서 뒤에서 모든 트래픽을 IP 하나에서 온 것으로 본 레이트 리밋 배포가 장애를 일으켰다."},"severity":"unknown","symptoms":["node-high-cpu","high-error-rate","high-latency","crash-loop","database-overloaded"],"timeline":{"detected_after":"2m","resolved_after":null},"date_kind":"incident","detection":{"en":"On 21 October automated monitoring detected higher error rates two minutes after the deploy. On 24 October no alert existed for 429 errors, and the team learned of the incident from customer reports that began at 18:10 UTC, a 62-minute detection delay. On 28 October on-call received a high error count alert. On 29 October the incident was reported with higher error rates and latency. No alert existed for CPU pressure, so the CPU saturation behind all three incidents was not seen directly.","ko":"21일에는 배포 2분 뒤 자동 모니터링이 오류율 상승을 잡았고, 28일에는 온콜이 높은 오류 수 경보를 받았다. PostHog 는 대부분의 경우 2분 안에 감지했다고 정리했다. 24일에는 429 오류 경보가 없어서 18:10 UTC 부터 들어온 고객 신고로 알았고, 감지까지 62분이 걸렸다. CPU 경보가 아예 없어서 공통 원인인 CPU 포화는 오래 드러나지 않았고, 29일에서 30일 사이 장애는 7시간 넘게 이어졌다."},"components":["node-pool","pod-scheduling","cache","database"],"root_cause":"capacity","remediation":[{"text":{"en":"rolled back the timeout handling change within 2 minutes on 21 October, which did not stop the errors","ko":"10월 21일 타임아웃 처리를 바꾼 배포를 2분 안에 롤백했지만 오류는 멈추지 않았다."}},{"text":{"en":"throttled connections and Redis writes on 21 October","ko":"10월 21일 23:00 UTC 부터 연결과 Redis 쓰기를 조절해 23:28 UTC 에 서비스를 복구했다."}},{"text":{"en":"halted the rollout on 28 October, scaled the US flags pods to zero until Redis recovered, then scaled them back gradually","ko":"10월 28일 롤아웃을 멈추고 US 플래그 파드를 0개로 줄여 Redis 가 회복되기를 기다린 뒤, 파드를 차례로 다시 올렸다."}},{"text":{"en":"rolled back hardware changes on 30 October at 00:03 UTC, which removed most errors but not the latency","ko":"10월 30일 00:03 UTC 에 하드웨어 변경을 롤백해 오류는 대부분 가라앉혔지만 지연은 남았다."}},{"text":{"en":"increased the pod resource requests and adjusted the Kubernetes fleet size so fewer pods ran on each node","ko":"파드 리소스 요청을 늘리고 쿠버네티스 플릿 크기를 조정해 노드당 파드 수를 줄였다."}},{"text":{"en":"added retry limits to Envoy between the load balancer and the service","ko":"로드밸런서와 서비스 사이 Envoy 재시도에 한도를 걸었다."}},{"text":{"en":"moved database connection timeouts and other critical settings to runtime configuration and raised the timeout values","ko":"데이터베이스 연결 타임아웃과 다른 중요한 설정을 런타임 설정으로 옮기고, 타임아웃 값을 피크 부하에 맞게 늘렸다."}},{"text":{"en":"added per-node and per-pod CPU alerts at 80% sustained for 5 minutes that page on-call","ko":"노드마다, 파드마다 CPU 사용률이 80% 를 5분 동안 넘으면 온콜을 호출하는 CPU 경보를 추가했다."}}],"action_items":[{"text":{"en":"Form a strike team from the flags, ingestion and infrastructure teams to review the application and infrastructure for remaining bottlenecks","ko":"플래그, 수집, 인프라 팀 엔지니어로 전담 팀을 꾸려 애플리케이션과 인프라 전체를 살피고 남은 병목을 찾는다."}},{"text":{"en":"Investigate separating the flags Redis instance from the application Redis instance","ko":"플래그 서비스의 Redis 를 본 애플리케이션의 Redis 와 분리하는 방안을 검토한다."}},{"text":{"en":"Before re-enabling ArgoCD sync, align the flags service chart configuration with the values in ArgoCD and define the deployment and Redis strategy","ko":"ArgoCD 동기화를 다시 켜기 전에 플래그 서비스 차트 설정을 ArgoCD 의 현재 값에 맞추고, 배포 전략과 Redis 전략을 정한다."}},{"text":{"en":"Make the feature flags team hard code-owners for flags-related code","ko":"플래그 관련 코드의 필수 코드 오너를 기능 플래그 팀으로 정한다."}},{"text":{"en":"Build a dashboard of key flag metrics with runbook links and update the feature flag runbooks","ko":"중요한 플래그 지표와 런북 링크를 한데 모은 대시보드를 만들고, 기능 플래그 런북을 고친다."}},{"text":{"en":"Add missing alerts on existing service and infrastructure metrics","ko":"기존 서비스 지표와 인프라 지표에 빠져 있는 경보를 추가한다."}},{"text":{"en":"Make readiness checks validate dependencies that degrade under load, for example by pinging the database","ko":"readiness 검사가 liveness 검사를 그대로 따라 하지 않고, 데이터베이스에 ping 을 보내는 식으로 부하가 걸렸을 때 약해졌던 의존성을 확인하게 바꾼다."}},{"text":{"en":"Rate-limit cache population so it cannot overwhelm Redis","ko":"캐시 채우기 작업에 레이트 리밋을 걸어 Redis 에 감당하지 못할 만큼 작업이 몰리지 않게 한다."}},{"text":{"en":"Monitor connection pools with automatic throttling near exhaustion and limit concurrent connections","ko":"연결 풀을 모니터링해 풀이 바닥에 가까워지면 자동으로 요청을 조절하고, 동시 연결 수에 상한을 둔다."}},{"text":{"en":"Build a production-scale load testing framework","ko":"운영 규모의 부하 시험 체계를 만든다."}},{"text":{"en":"Build progressive rollout infrastructure to limit blast radius","ko":"점진적 롤아웃 기반을 만들어 배포가 미치는 범위를 줄인다."}},{"text":{"en":"Document Postgres-to-Redis data flows and write runbooks for data transfer storms","ko":"Postgres 에서 Redis 로 가는 데이터 흐름을 문서로 정리하고, 데이터 전송이 한꺼번에 몰릴 때 쓸 런북을 만든다."}}],"contributing":["no-alert-on-limit","retry-storm","revert-did-not-restore","single-point-of-failure","no-staged-rollout","staging-not-representative"],"recurrence_of":null,"symptoms_text":{"en":"On 21 October 2025 at 21:45 UTC a change to timeout handling was deployed to the feature flags service, and two minutes later automated monitoring reported higher error rates. The change was rolled back by 21:49, but error rates stayed high: about 38% of flag evaluation requests in the US datacenter returned errors. At 22:30 Redis metrics showed memory exhaustion on ElastiCache, at 22:35 Postgres connections spiked, and at 22:45 the team found a massive data transfer from Postgres into Redis. The main PostHog application was affected as well, because it shares the Redis instance with the flags service.\n\nOn 24 October IP-based rate limiting was deployed to the /flags endpoint at 18:00 UTC, and from 18:01 about 97% of evaluation requests worldwide received 429 (rate limit) errors. No alert existed for 429 errors, so the team learned of it from customer reports, with a 62-minute detection delay; the service recovered at 19:12 when rate limiting was disabled.\n\nOn 28 October a routine deployment with no flags code changes rolled the feature flag pods in the US region. From 19:15 the new pods could not connect to Postgres within the 20-second startup timeout and went into crash loops. Redis writes spiked from 19:20, Redis began evicting keys at 19:25 so the cache was effectively gone, the main PostHog app began to suffer at 19:26, and about 34% of US requests failed. On-call was paged by a high error count alert at 19:23.\n\nFrom 22:30 UTC on 29 October to 05:39 UTC on 30 October queries from the flags service to Redis and Postgres were slow even though the metrics of both showed them healthy. Response times were several times the usual. Node CPU was above 90%, but no CPU alert existed, so this went unnoticed as the cause for more than 7 hours. Rolling back hardware changes at 00:03 removed most of the errors, but the latency stayed.\n\nCustomers saw errors and slow responses for all flag types, including read-only evaluations. On 28 October the team first tried the same remediation as on 21 October before finding other ways to reduce parallelism.","ko":"2025-10-21 21:45 UTC, 타임아웃 오류를 줄이려는 배포 직후 자동 모니터링이 기능 플래그 서비스의 오류율 상승을 잡았다. 2분 만에 롤백했지만 오류율은 내려가지 않았고, US 데이터센터에서 평가 요청의 약 38% 가 오류를 냈다.\n\n21일에는 ElastiCache 의 Redis 메모리가 바닥났고, Postgres 연결이 급증해 연결 풀이 감당하지 못했다. 이어서 Postgres 에서 Redis 로 대량의 데이터가 옮겨지고 있는 것이 보였다. 같은 Redis 를 쓰는 PostHog 본 애플리케이션까지 영향을 받았다.\n\n10월 28일에는 플래그 코드를 바꾸지 않은 일상 배포가 US 리전의 플래그 파드 롤아웃을 일으켰다. 새 파드는 20초 시작 타임아웃 안에 Postgres 에 연결하지 못해 크래시 루프에 빠졌다. Redis 쓰기가 폭증해 키 축출이 일어나면서 캐시를 사실상 쓸 수 없게 됐고, US 요청의 약 34% 가 실패했다. 팀은 처음에 21일과 같은 대응을 시도했다.\n\n10월 24일에는 IP 기반 레이트 리밋을 배포한 직후 전 세계 평가 요청의 약 97% 가 429 오류를 받았다. 429 경보가 없어서 고객 신고로 알았다. 10월 29일 22:30 UTC 부터 30일 05:39 UTC 까지는 오류율과 지연이 올라갔지만 Redis 와 Postgres 지표는 정상이었다. 실제 원인은 노드 CPU 사용률이 90% 를 넘은 것이었고, 응답 시간이 평소의 몇 배로 늘었다. CPU 경보가 아예 없어서 7시간 넘게 원인을 잡지 못했다."},"updated_at":"2026-11-10T00:00:33.095020Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0100","org_slug":"posthog"}