{"id":"IL-0390","org":"PostHog","date":"2025-11-11","docs":["https://www.postgresql.org/docs/current/storage-toast.html#STORAGE-TOAST-ONDISK","https://wiki.postgresql.org/wiki/TOAST","https://status.posthog.com/incidents/3yqn2wwkj5rq"],"title":{"en":"PostHog US Cloud event ingestion slowed to delays of up to 2 days as writes to the Persons Postgres table stalled","ko":"PostHog US Cloud 의 Persons Postgres 데이터베이스 쓰기가 크게 느려져 이벤트와 persons 수집이 최대 2일 지연됐다"},"domain":["database","aws"],"impact":{"scope":"Event and persons ingestion for US Cloud, delayed from 10 minutes up to 2 days; analytics on recent data, feature flags and experiments using person properties, filtering in error tracking and session replay, and CDP destinations and workflows were affected","users":"All customers in the US region","region":"US Cloud"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"TOAST OID exhaustion degrades writes long before the hard limit of about 4 billion OIDs, it is rare even at large scale, and standard dashboards for CPU, memory, IOPS and lock contention did not immediately point at it, so diagnosis took a day and a half without monitoring of TOAST growth, OID usage or disk reads per write. Many core features depended on timely updates to the persons table, so its slowdown reached a wide part of the product. The first backfill approach lacked visibility for a long migration, the Kafka tiered storage settings and alerts were not tuned for a large backlog, and the people who normally handle customer communications were all unavailable.","ko":"Persons 테이블 하나에 analytics, feature flag, replay 필터, CDP 같은 핵심 기능 다수가 기대고 있어, 이 테이블이 느려지자 제품의 넓은 부분이 영향을 받았다. TOAST 증가, OID 사용량, 쓰기당 디스크 읽기 증폭을 보는 모니터링이 없어 OID 고갈을 원인으로 확정하는 데 하루 반쯤 걸렸다. 처음의 백필 방식은 길고 큰 마이그레이션에 필요한 가시성과 견고함이 모자라 장애 중에 Dagster 로 옮겨야 했다. MSK 디스크가 차오른 일로 tiered storage 설정과 알림이 큰 백로그 상황에 맞춰져 있지 않았다는 것이 드러났다. 평소 고객 소통을 맡는 팀원이 장애 내내 모두 자리에 없어 대신할 사람을 급히 찾아야 했다.\n\nPostHog 는 잘된 점으로 네 가지를 꼽았다. 데이터를 잃지 않았다. 여러 팀과 외부 컨설턴트, AWS 엔지니어가 함께 드문 엔진 수준 문제를 진단했다. 시스템을 살려 둔 채 트리거와 백필로 새 Persons 테이블로 옮겼다. 엔지니어가 직접 정기적으로 상태를 알렸다."},"related":[],"sources":[{"url":"https://github.com/PostHog/post-mortems/blob/main/2025-11-15-persons-db-migration.md","kind":"github","archived":null}],"summary":{"en":"PostgreSQL stores large column values (>2KB compressed) out of line in a TOAST table, and each such value gets a unique OID from a finite 32-bit space of about 4 billion values. PostHog's Persons table has a properties column that often exceeds that threshold, so it has many TOASTed values. As used OIDs approached the limit, Postgres had to read through long runs of used OIDs to find a free one for each INSERT or UPDATE of a TOASTed row, and writes to the Persons database kept waiting for OIDs. Write performance became severely degraded well before the hard limit, and event ingestion for US Cloud ground to a halt from 11 November 16:02 UTC.\n\nThe cause was identified on 12 November at 10:17pm UTC with AWS RDS engineers and external consultants, and the only option left was a migration to a new partitioned table. PostHog created a partitioned Persons table with a fresh TOAST table and OID space, kept old and new tables in sync with database triggers, copied existing Person data with a backfill script, and changed ingestion and application code so that all writes went to the new table and reads tried it first, falling back to the old one. The web app was deployed under manual control, and engineers worked in shifts through the weekend on the feature flags API, error tracking ingestion and the Django web application. Ingestion was healthy again on 14 November at 15:02 UTC.\n\nWhile ingestion workers were scaled up to clear the backlog, local disk on the AWS MSK cluster reached about 85%, so PostHog paused ingestion, reduced the topic's local retention and resumed once there was room. The backfill was moved into Dagster for better visibility. By 15 November at 6:20 AM UTC all events since the start had been processed. No data was lost.","ko":"2025년 11월 11일 16:02 UTC 부터 11월 14일 15:02 UTC 까지 PostHog 의 수집 처리 파이프라인 성능이 크게 떨어져, US 리전 모든 고객의 이벤트 처리가 최대 2일 지연됐다. 원인은 Person 정보를 저장하는 Postgres 데이터베이스가 person 속성을 담는 JSONb 열과 관련해 전에 겪은 적 없는 Postgres 한계에 닿은 것이었다. PostgreSQL 은 압축 뒤 2KB 를 넘는 큰 열 값을 TOAST 테이블이라는 별도 테이블에 저장하고, 옮긴 값마다 약 40억 개 크기의 32비트 공간에서 고유한 OID 를 하나씩 할당한다. OID 는 객체 식별자이고, 전역 카운터에서 나오며 40억 개마다 한 바퀴 돈다. 이미 쓴 값이 나오면 Postgres 는 다음 값으로 다시 시도한다. PostHog 의 Persons 테이블은 properties 열이 자주 2KB 를 넘어 TOAST 값이 아주 많았다. 쓴 OID 가 한계에 가까워지자 INSERT 나 UPDATE 한 건마다 비어 있는 OID 를 찾느라 엄청난 양을 읽게 됐다. 쓰기 쿼리 하나의 디스크 읽기는 10kb 에서 15MB 로, 지연은 100배로 늘었고 이벤트 수집이 거의 멈췄다.\n\n근본 원인은 문제가 시작되고 하루 반이 지난 11월 12일 22:17 UTC 에야 찾았다. OID 고갈은 큰 규모에서도 드물게 겪는 현상이었다. 표준 대시보드는 이것을 곧바로 가리키지 않았고, TOAST 와 OID 사용량을 보는 관측 수단도 없었다. PostHog 는 AWS RDS 팀 엔지니어와 외부 컨설턴트의 도움을 받았고, 새 파티션 테이블로 옮기는 것 말고는 방법이 없다는 결론에 이르렀다. 새 TOAST 테이블과 OID 공간을 갖는 파티션 Persons 테이블을 만들고, 트리거로 옛 테이블과 새 테이블을 맞추며 기존 데이터를 백필했다. 쓰기는 새 테이블로 보내고 읽기는 새 테이블을 먼저 보고 옛 테이블로 넘어가게 바꿨다. 장애 중 새 문제를 만들지 않으려고 웹 앱은 사람이 통제하는 배포로 내보냈다.\n\n11월 14일 15:02 UTC 에 수집이 정상으로 돌아왔고, PostHog 는 밀린 이벤트 처리에 집중했다. 이때 AWS MSK 에서 따로 문제가 났다. MSK 의 tiered storage 는 최근 4시간치 데이터를 로컬 디스크에 두었다가 S3 로 넘긴다. 밀린 데이터를 따라잡는 동안 이 4시간치가 유난히 커져 로컬 디스크 사용률이 85% 에 닿았다. 수집을 잠시 멈추고 해당 토픽의 로컬 보존 기간을 줄인 뒤, 디스크 여유를 확인하고 수집을 다시 시작했다. 오래 걸릴 백필은 Dagster 로 옮겼다. 11월 15일 06:20 UTC 에 밀린 이벤트를 모두 처리해 완전히 복구했고, 잃은 데이터는 없었다."},"trigger":{"en":"The 32-bit OID space used to track TOASTed values of the Persons table's properties column, a JSONb field that often exceeds the 2KB threshold for out-of-line storage, was nearly exhausted.","ko":"Persons 테이블의 properties 열은 자주 2KB 를 넘어 TOAST 테이블에 따로 저장되는데, 이 값마다 할당하는 32비트 OID 공간이 거의 다 찼다. 그러자 쓰기 한 건마다 비어 있는 OID 를 찾느라 이미 쓴 OID 를 길게 훑어야 했다."},"severity":"unknown","symptoms":["throughput-degraded","high-latency"],"timeline":{"detected_after":null,"resolved_after":"3d 14h 18m"},"date_kind":"incident","detection":null,"components":["database","message-broker"],"root_cause":"capacity","remediation":[{"text":{"en":"Created a new partitioned Persons table with a fresh TOAST table and OID space","ko":"새 TOAST 테이블과 새 OID 공간을 갖는 파티션 Persons 테이블 구조를 새로 만들었다."}},{"text":{"en":"Added database triggers to keep the old and new Persons tables in sync for live writes","ko":"실시간 쓰기가 옛 테이블과 새 테이블에 함께 맞춰지도록 데이터베이스 트리거를 넣었다."}},{"text":{"en":"Copied the existing Person data from the old table to the new one with a backfill script","ko":"백필 스크립트로 기존 Person 데이터를 옛 테이블에서 새 테이블로 복사했다."}},{"text":{"en":"Changed ingestion and application code to write to the new table and read from it first, with fallback to the old table","ko":"새 쓰기는 모두 새 테이블로 가고, 옮기는 동안 읽기는 새 테이블을 먼저 본 뒤 옛 테이블로 넘어가도록 수집과 애플리케이션 로직을 바꿨다."}},{"text":{"en":"Deployed the web app changes to production under manual control rather than a fully automated rollout","ko":"장애 중 새 문제를 만들 위험이 있어, 웹 앱은 완전 자동 롤아웃 대신 사람이 통제하는 배포로 운영에 내보냈다. 엔지니어 여럿이 주말 내내 교대로 Feature flags API, Error tracking 수집, Django 웹 애플리케이션을 고쳤다."}},{"text":{"en":"Scaled ingestion workers to process the accumulated backlog","ko":"새 Persons 테이블에서 수집 성능이 돌아온 뒤, 파티션별 지연, 처리량, 자원 사용률을 지켜보며 수집 워커를 늘려 밀린 이벤트를 처리했다."}},{"text":{"en":"Paused ingestion when the MSK local disk reached about 85%","ko":"MSK 로컬 디스크 사용률이 약 85% 에 닿자 수집을 잠시 멈췄다."}},{"text":{"en":"Reduced the local retention of the affected Kafka topic","ko":"해당 Kafka 토픽의 로컬 보존 기간을 줄여, S3 로 넘기기 전에 디스크에 두는 데이터를 줄였다."}},{"text":{"en":"Resumed ingestion after confirming there was enough disk headroom","ko":"디스크 여유가 충분한 것을 확인한 뒤 수집을 다시 시작했다."}},{"text":{"en":"Moved the backfill into Dagster to finish it over the weekend","ko":"오래 걸리는 백필 작업을 더 잘 지켜보고 안정적으로 돌리려고 백필 과정을 Dagster 로 옮겼고, 남은 백필과 정리 작업을 주말 동안 Dagster 로 끝냈다."}}],"action_items":[{"text":{"en":"Add metrics and alerts for heavy disk read amplification per write","ko":"쓰기마다 디스크 읽기가 크게 늘어나는 현상에 지표와 알림을 더한다."}},{"text":{"en":"Add metrics and alerts for TOAST table statistics","ko":"TOAST 테이블 통계에 지표와 알림을 더한다."}},{"text":{"en":"Add metrics and alerts for other Postgres engine limits that matter at large scale","ko":"큰 규모에서 문제가 될 수 있는 다른 Postgres 엔진 한계에도 지표와 알림을 더한다."}},{"text":{"en":"Write runbooks for engine-level Postgres limits, with decision trees for migrating versus repairing in place","ko":"비슷한 Postgres 엔진 수준 문제의 증상과 진단 방법을 런북으로 정리하고, 옮길지 제자리에서 고칠지 정하는 의사 결정 트리를 분명하게 넣는다."}},{"text":{"en":"Create customer communication runbooks with a clear escalation path and redundancies","ko":"고객에게 언제 어떻게 알릴지, 누구에게 에스컬레이션할지, 누가 대신 맡을지를 분명히 정한 고객 소통 런북을 새로 만든다."}},{"text":{"en":"Evaluate other data stores for the persons database","ko":"persons 데이터베이스에 다른 데이터 저장소를 쓰는 방안을 계속 검토한다."}},{"text":{"en":"Complete the reconsolidation on the new Persons tables","ko":"새 테이블로 통합하는 작업을 마무리한다."}},{"text":{"en":"Improve the backfill tooling and the safeguards for Kafka tiered storage","ko":"백필 도구와 Kafka tiered storage 의 안전장치를 개선한다."}},{"text":{"en":"Design and operate Person-like tables so they avoid similar limits","ko":"Person 과 비슷한 테이블을 처음부터 비슷한 한계에 닿지 않게 설계하고 운영한다."}}],"contributing":["limit-unknown","monitoring-blind-spot","no-runbook"],"recurrence_of":null,"symptoms_text":{"en":"From 11 November 2025 at 16:02 UTC, PostHog's event ingestion pipeline for US Cloud became severely degraded, and events for all customers in the US region were processed with delays from 10 minutes up to 2 days. Writes to the Postgres database that stores Person information could take multiple seconds per update, and the pipeline slowed until no standard scaling option helped.\n\nRecent events did not show up in the product. Charts and queries over recent time ranges were inaccurate, feature flags that relied on person properties evaluated with delayed properties, filtering of errors and session replays by Person updates was affected, and CDP destinations and workflows that relied on Persons ran late. Ingestion of errors and replays itself stayed healthy.\n\nStandard dashboards for CPU, memory, IOPS and lock contention did not point at the cause, and there was no monitoring of TOAST table size, OID usage or disk reads per write on a table. The root cause was found only on 12 November at 10:17pm UTC, a day and a half after the start, with help from AWS RDS engineers and external consultants. Disk reads per write query had grown from 10kb to 15MB, and the latency of those queries had risen 100x.\n\nDuring the catch-up, local disk usage on the AWS MSK (Kafka) cluster reached about 85% and triggered alerts, because tiered storage keeps the most recent 4 hours of data on local disk before moving it to S3, and the backlog made that window unusually dense.","ko":"2025년 11월 11일 16:02 UTC 부터 PostHog 수집 처리 파이프라인의 성능이 크게 떨어져, US 리전 모든 고객의 이벤트 처리가 지연되기 시작했다. 고객이 최근에 보낸 이벤트가 PostHog 에 보이지 않았다.\n\nPersons 데이터베이스의 쓰기는 TOAST 테이블에서 쓸 수 있는 OID 를 기다리느라 업데이트 한 건에 몇 초씩 걸렸다. 쓰기 쿼리 하나가 읽는 디스크 양은 10kb 에서 15MB 로 늘었고, 이 쿼리들의 지연은 100배가 됐다. 수집 파이프라인은 보통의 확장 수단으로는 손쓸 수 없을 만큼 느려졌다.\n\n지연은 고객에 따라 10분에서 최대 2일이었다. 최근 기간이 들어간 Analytics 차트와 쿼리는 부정확했고, person 속성을 쓰는 feature flag 는 그 속성이 늦게 반영됐다. Error tracking 과 Session replay 수집은 정상이었지만 Person 업데이트로 거르는 필터는 영향을 받았고, Persons 에 기대는 CDP destination 과 Workflow 는 늦게 실행됐다.\n\nCPU, 메모리, IOPS, 잠금 경합을 보는 표준 대시보드는 OID 고갈을 곧바로 가리키지 않았다. TOAST 테이블 크기나 OID 사용량, 특정 테이블의 쓰기당 디스크 읽기 증폭을 보는 관측 수단이 없어서, 근본 원인을 찾는 데 하루 반이 걸렸다.\n\n11월 14일 수집이 회복된 뒤 밀린 이벤트를 처리하는 동안 AWS MSK 의 로컬 디스크 사용률이 약 85% 까지 올라 알림이 울렸다."},"updated_at":"2026-11-14T12:34:04.114054Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0390","org_slug":"posthog"}