{"id":"IL-0214","org":"PagerDuty","date":"2025-08-28","docs":[],"title":{"en":"PagerDuty's Kafka brokers ran out of JVM heap one after another, and the event APIs rejected events with 502 responses","ko":"Kafka 브로커들이 JVM 힙을 다 써 클러스터 전체로 장애가 번지며 PagerDuty 미국 리전의 이벤트 수집과 알림이 멈추거나 늦어졌다"},"domain":["database"],"impact":{"scope":"Event ingestion and asynchronous processing: up to about 95% of API events rejected with 502 at the peak for 38 minutes, 18.87% of create requests and 4.35% of update requests failed with 5xx, notifications, webhooks, integrations, chat messages and the REST API delayed, duplicated or failing; no accepted events lost","users":"some customers in the US Service Regions; fewer than 10% of customers saw errors in non-event incident creation and 6.06% of users could not acknowledge or resolve incidents through the mobile app","region":"US Service Regions"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"PagerDuty found its message processing more intricate than it had thought: some services had to be restarted to pick up changes, some API activity produced far more messages than expected, and it was hard to tell which parts of the system put heavy load on Kafka. Because producers and JVM heap were barely observed, responders could not see the first failure until several downstream systems were affected, and no one on the call held a model of the whole system. Enabling the feature through configuration rather than a separate deployment was meant as a safety step but made the outage harder to link to the change. Runbooks and backfill scripts were imperfect but gave responders a head start, and the second incident was mitigated in about 50 minutes by repeating the first incident's steps.","ko":"PagerDuty 는 연쇄 장애가 본래 예측하기 어렵고, 한 서비스의 작은 문제가 시스템 도표로는 보이지 않는 길로 다른 서비스에 번진다고 봤다. 관측성의 빈틈 때문에 대응자는 하위 시스템 여러 개가 망가질 때까지 처음 무너진 것을 보지 못했다. 사람은 의존 관계를 직선으로 생각하지만 운영 시스템은 그렇게 움직이지 않는다. 통화에 참여한 누구도 시스템 전체를 머릿속에 담고 있지 않아 의존 관계를 잇는 데 시간이 걸렸다.\n\n새 기능은 안전하게 내려고 별도 배포 대신 설정으로 조금씩 켰는데, 이 때문에 장애와 변경을 바로 잇기 어려웠다. 이 기능은 잘 쓰지 않는 프레임워크와 라이브러리로 만들어서, 요청마다 프로듀서를 새로 만든다는 것이 코드에 드러나지 않았다. 여기에 Kafka 알림이 적었던 것이 겹쳐 점진적 롤아웃 절차가 있어도 장애를 막지 못했다. 중요한 알림이 우선순위 낮은 웹훅 알림에 묻혔고, 백업 절차로 상태 페이지에 공지하는 일처럼 일부 운영 절차는 더 자주 연습해야 했다. 미리 만들어 둔 백필 스크립트와, 처리를 일시 정지했다가 재개하는 런북은 완벽하지 않았지만 대응자에게 출발점을 줬다."},"related":[],"sources":[{"url":"https://www.pagerduty.com/eng/august-28-kafka-outages-what-happened-and-how-were-improving/","kind":"blog","archived":"https://web.archive.org/web/20260519152300/https://www.pagerduty.com/eng/august-28-kafka-outages-what-happened-and-how-were-improving/"}],"summary":{"en":"Kafka is the backbone of PagerDuty's asynchronous architecture. Kafka brokers keep metadata and state for every active producer, identified by a producer ID. A feature giving customers more auditability of API key usage published to a new Kafka topic at roughly the volume of API requests, hundreds of millions per day. It used the pekko-connectors-kafka Scala library, a framework rarely used at PagerDuty, and passing Kafka settings to it instead of a pre-created producer made it create a new producer on every send(). As the rollout grew, the brokers tracked millions of extra producers and their heap use rose slowly but steadily; given how fast producers were created, tuning transaction.max.timeout.ms or producer.id.expiration.ms would not have helped.\n\nAt 03:53 UTC on 28 August the brokers exhausted their JVM heap, starting with one broker and spreading over the cluster. Responders expanded the cluster and removed the first broker, then doubled the heap with a rolling restart, which stabilised Kafka. The rest of the first incident went into recovering the many services that depend on Kafka; several needed manual intervention, and the ksqlDB stream management runbooks lacked verification steps, which led to data loss that had to be backfilled with scripts that had not been tested recently. Status page updates were drafted but did not appear, so backup procedures were used to add them by hand. All services were back to normal by 10:10 UTC.\n\nThe team that owned the feature did not know it was linked to the outage and raised its rollout to 75%. At 16:38 UTC Kafka degraded again; on-call responders doubled the heap again with a rolling restart and limited customer impact to about 50 minutes. Having seen the pattern repeat, responders connected the heap growth to the gradual rollout and rolled the feature back with a pull request that stopped it from publishing to Kafka. All services were fully restored by 20:24 UTC, and later analysis of the code and logs found the producer-per-request error.","ko":"PagerDuty 의 비동기 구조는 Kafka 를 중심에 둔다. Kafka 브로커는 활성 프로듀서마다 트랜잭션과 메시지 순번 같은 메타데이터를 추적한다. 고객에게 API 와 키 사용 내역을 보여 주려는 새 기능은 하루 수억 건인 API 사용량만큼의 메시지를 Kafka 토픽에 쓰는데, 논리 오류 때문에 프로듀서 하나를 다시 쓰지 않고 요청마다 새 프로듀서를 만들었다. pekko-connectors-kafka Scala 라이브러리에 미리 만든 프로듀서 대신 Kafka 설정을 넘기면 send() 를 부를 때마다 새 프로듀서가 생겼고, 명시적인 new 가 없어 코드에서 눈에 띄지 않았다. 정점에서는 평소의 84배인 시간당 약 420만 개의 프로듀서를 추가로 추적했고, 이 속도에서는 transaction.max.timeout.ms 와 producer.id.expiration.ms 를 조정해도 막을 수 없었다.\n\n2025년 8월 28일 03:53 UTC 에 브로커 하나에서 JVM 힙이 바닥났고 문제는 클러스터 전체로 번졌다. 대응 팀은 하드웨어를 의심해 클러스터를 늘리고 그 브로커를 뺐다가, 다른 브로커도 메모리가 바닥나자 소프트웨어 문제임을 알았다. 힙 크기를 두 배로 늘려 클러스터 전체를 롤링 재시작하자 Kafka 가 안정됐고, 그 뒤로는 강하게 엮인 하위 서비스들을 되살리는 데 시간을 썼다. 몇몇 애플리케이션은 손으로 복구해야 했고, ksqlDB 스트림 관리 런북에 확인 단계가 없어 생긴 데이터 손실은 최근에 시험하지 않은 스크립트로 백필했다. 공개 상태 페이지 자동 게시가 실패해 백업 절차로 손으로 공지를 올렸다. 모든 서비스는 10:10 UTC 에 정상으로 돌아왔다. 처음에는 Kafka 를 안정시키는 데 집중해 촉발 원인이 보이지 않았으므로 기능을 되돌리지 않았다.\n\n새 기능을 맡은 팀은 자기 변경이 장애와 관련 있다는 것을 몰랐고, 시스템이 안정되자 롤아웃을 25% 에서 75% 로 올렸다. 트래픽이 늘어 힙이 훨씬 빨리 찼고, 16:38 UTC 에 두 번째 장애가 시작됐다. 당번 대응자는 힙을 다시 두 배로 늘려 롤링 재시작했고 고객 영향은 50분으로 줄었다. 대응 팀은 기능 롤아웃과 힙 증가의 강한 상관관계를 찾아 Kafka 메시지를 더는 보내지 않게 하는 풀 리퀘스트로 기능을 되돌렸고, 20:24 UTC 에 모든 서비스가 복구됐다. Transactional Outbox 패턴 덕분에 이미 받은 이벤트나 데이터는 잃지 않았고, Kafka 가 돌아온 뒤 쌓인 메시지가 발행되면서 알림이 늦게 갔다."},"trigger":{"en":"A new feature that recorded API key usage in a Kafka topic was being rolled out by configuration: 1% on 21 August, 5% and then 25% on 27 August, and 75% on 28 August. A logical error in the feature created a new Kafka producer for every API request instead of reusing one producer.","ko":"API 키 사용 내역을 Kafka 토픽에 남기는 새 기능의 논리 오류로 API 요청마다 새 Kafka 프로듀서가 만들어졌다. 이 기능은 8월 21일 1% 에서 시작해 27일 5%, 같은 날 저녁 25%, 28일 75% 로 롤아웃됐다."},"severity":"unknown","symptoms":["memory-pressure","high-error-rate","throughput-degraded"],"timeline":{"detected_after":null,"resolved_after":"6h 17m"},"date_kind":"incident","detection":{"en":"Alerts pointed to one failing Kafka broker at the start of the first incident, and alerts fired again at 16:38 UTC when Kafka performance degraded for the second time. Critical alerts about core API errors were buried among webhook pages.","ko":"03:53 UTC 에 장애가 시작되자 처음 온 알림은 Kafka 브로커 하나의 고장을 가리켰다. 두 번째 장애는 16:38 UTC 에 Kafka 성능이 다시 떨어지며 울린 알림으로 알았다. 핵심 API 오류를 알리는 중요한 알림은 웹훅 호출에 묻혔다."},"components":["message-broker"],"root_cause":"deploy-rollout","remediation":[{"text":{"en":"Expanded the Kafka cluster and removed the first failing broker","ko":"처음 알림이 브로커 하나의 고장을 가리켜 하드웨어 문제를 의심했고, 클러스터를 늘리고 그 브로커를 뺐다."}},{"text":{"en":"Doubled the Kafka broker heap size and applied it with a rolling restart across the cluster","ko":"원인을 조사할 여유를 얻으려고 Kafka 힙 크기를 두 배로 늘렸고, 이를 적용하려고 클러스터 전체를 롤링 재시작해 Kafka 를 안정시켰다."}},{"text":{"en":"Added status page updates by hand through backup procedures when automated publishing failed","ko":"상태 페이지 자동 게시가 되지 않자 엔지니어링 팀을 더 부르고 백업 절차를 시작해 상태 페이지에 손으로 공지를 올렸다."}},{"text":{"en":"Recovered Kafka-dependent services, some by manual intervention, and backfilled lost data with scripts","ko":"Kafka 가 돌아온 뒤 여러 애플리케이션을 손으로 복구했고, ksqlDB 스트림 관리 중 생긴 데이터 손실은 스크립트로 백필했다."}},{"text":{"en":"In the second incident, doubled the heap again with another rolling restart","ko":"두 번째 장애 때 당번 대응자가 힙 크기를 다시 두 배로 늘리고 롤링 재시작했다."}},{"text":{"en":"Rolled back the API usage feature with a pull request that stopped it from publishing Kafka messages","ko":"API 사용 기능의 롤아웃과 힙 증가 사이의 강한 상관관계를 찾아, 이 기능이 Kafka 메시지를 더는 보내지 않게 하는 풀 리퀘스트로 기능을 되돌렸다."}}],"action_items":[{"text":{"en":"Remove the dependency that blocked status page automation and simplify and republish the manual runbook","ko":"상태 페이지 자동 게시를 막은 의존을 없앴고, 손으로 공지하는 런북을 단순하게 고쳐 다시 냈다."}},{"text":{"en":"Run live drills regularly so status updates go out even when parts of the platform are degraded","ko":"플랫폼 일부가 망가져도 상태 공지가 빨리 나가도록 실전 연습을 평소 업무에 더 꾸준히 넣는다."}},{"text":{"en":"Expand JVM and Kafka monitoring of heap, garbage collection and producer and consumer health","ko":"힙, 가비지 컬렉션, 프로듀서와 컨슈머 상태 같은 JVM 과 Kafka 수준의 모니터링을 넓혀 가용성에 영향이 가기 전에 부하 신호를 드러낸다."}},{"text":{"en":"Strengthen service dependency mapping so cascading failures are easier to trace","ko":"서비스 의존 관계 지도를 보강해 대응 중에 연쇄 장애를 더 쉽게 따라가게 한다."}},{"text":{"en":"Collect customer impact metrics into incident workflows automatically","ko":"고객 영향 지표 수집을 인시던트 작업 흐름에 자동으로 넣어 대응자가 영향 범위를 늘 볼 수 있게 한다."}},{"text":{"en":"Add stricter change management guardrails, with readiness checklists and slower ramp-up windows for major rollouts","ko":"큰 롤아웃에 준비 점검표와 더 느린 증가 기간 같은 변경 관리 장치를 더 엄격하게 두고, 큰 장애 직후에는 특히 그렇게 한다."}},{"text":{"en":"Improve recovery mechanisms in the streaming systems","ko":"스트리밍 시스템의 복구 방법을 고쳐 장애가 길어질 가능성을 줄인다."}},{"text":{"en":"Automate communication workflows such as status page updates, internal and external syncs and reminders","ko":"상태 페이지 공지, 내부와 외부 동기화, 알림 같은 소통 작업을 자동화해 혼선을 줄이고 이해관계자가 같은 정보를 보게 한다."}},{"text":{"en":"Test major incident workflows monthly with chaos engineering, such as status page failover","ko":"상태 페이지 장애 조치와 PD-on-PD 절차 같은 큰 장애 대응 흐름을 카오스 엔지니어링으로 매달 시험해 자동화에 덜 기댄다."}},{"text":{"en":"Define clearer incident severity levels and roles, including dedicated communications liaisons","ko":"인시던트 심각도 등급과 전담 소통 담당을 포함한 역할 정의를 더 분명히 해 혼란을 줄이고 대응을 빠르게 한다."}}],"contributing":["monitoring-blind-spot","alert-noise","delayed-effect-change","blast-radius-too-wide"],"recurrence_of":null,"symptoms_text":{"en":"When a Kafka cluster of PagerDuty's US Service Regions began failing at 03:53 UTC on 28 August 2025, the first alerts pointed to one failing broker. More brokers then ran out of memory, and PagerDuty's event APIs began rejecting incoming events with a response status code of 502 while notifications, webhooks, chat integrations and the REST API degraded.\n\nInside Kafka, the brokers came under heavy JVM memory pressure, went into GC thrashing and finally exhausted the JVM heap. The problem started on one broker and quickly spread across the whole cluster, until services could not talk to Kafka at all and errors chained through the downstream services. The heap was filling because the brokers were tracking nearly 4.2 million extra producers per hour at peak, 84 times the usual number of new producers, but there was little visibility into tracked producers and JVM heap usage. Of the 19 high-urgency pages during the incident, 18 were about webhooks, which hid the signals about core API errors, and the public status page was not updated for the first two hours because its automated publishing failed.\n\nCustomers saw up to about 95% of API events rejected with 502 at the peak, for 38 minutes, 18.87% of create requests and 4.35% of update requests answered with 5xx, about 23% of notifications delayed by 5 minutes or more, delayed or dropped integration events for Jira, ServiceNow, Salesforce and Zendesk, duplicated webhooks and chat messages, and 400 errors in the iOS and Android apps when viewing incidents. No accepted events were lost; messages held in a transactional outbox were published once Kafka was back, which caused the delays.\n\nResponders first suspected a hardware problem on the single broker, so they expanded the cluster and removed that broker. Only when more brokers ran out of memory did they treat it as a systemic software problem. Doubling the heap stabilised Kafka, but the trigger was not evident, so nothing was rolled back. Later that day the feature behind it was raised from 25% to 75% of traffic, the enlarged heap filled within about two hours, and at 16:38 UTC alerts fired again as Kafka performance degraded.","ko":"2025년 8월 28일 03:53 UTC 에 PagerDuty 의 Kafka 에서 장애가 시작됐다. 처음 온 알림은 Kafka 브로커 하나의 고장을 가리켰지만, 곧 다른 브로커들도 메모리가 바닥났다. 미국 서비스 리전의 일부 고객은 새 이벤트가 처리되지 않거나 늦게 처리됐다.\n\n브로커의 JVM 힙 사용량이 천천히 꾸준하게 올라 가비지 컬렉션이 쉬지 않고 도는 GC thrash 가 생겼고, 결국 힙이 바닥났다. 브로커 하나에서 시작한 문제가 클러스터 전체로 번졌고, Kafka 에 기대는 서비스들은 끝내 Kafka 와 전혀 통신하지 못해 하위 서비스에서 오류가 줄줄이 났다. 힙이 찬 것은 브로커가 정점에서 시간당 약 420만 개의 프로듀서를 추가로 추적했기 때문이었고, 이는 평소 새 프로듀서 수의 84배였다.\n\n영향이 가장 클 때 이벤트 API 는 들어오는 이벤트를 502 응답 코드로 거부했고, 정점에서는 약 95% 를 38분 동안 거부했다. 생성 요청의 18.87% 와 갱신 요청의 4.35% 가 5xx 응답을 받았고, REST API 오류율이 올랐다. 알림의 약 23% 가 5분 넘게 늦었다. 웹훅은 늦거나 빠지거나 두 번 갔고, iOS 와 Android 앱은 인시던트를 볼 때 400 오류를 더 많이 받았다. Jira, ServiceNow, Salesforce, Zendesk 연동 이벤트가 늦거나 빠졌고, 채팅 메시지가 두 번 가거나 늦게 처리됐다. 이미 받은 이벤트는 잃지 않았다. 보낼 메시지를 먼저 데이터베이스에 적어 두는 Transactional Outbox 에 남은 메시지가 Kafka 가 돌아온 뒤 발행되면서 처리가 늦어졌다.\n\n대응 팀은 처음에 브로커 하나의 하드웨어 문제를 의심해 클러스터를 늘리고 그 브로커를 뺐다. 다른 브로커들도 메모리가 바닥나고서야 하드웨어가 아니라 시스템 전체의 소프트웨어 문제라는 것을 알았다.\n\n장애 중 high-urgency 호출 19건 가운데 18건이 웹훅 관련 알림이어서 대응 팀은 핵심 API 오류 신호를 놓쳤다. Kafka 의 프로듀서 추적과 JVM 힙 사용량이 잘 보이지 않아 원인을 찾기 어려웠다.\n\n공개 상태 페이지의 자동 게시가 실행되지 않아 내부에서 쓴 공지가 처음 두 시간 동안 페이지에 나타나지 않았다.\n\n16:38 UTC 에 Kafka 성능이 다시 떨어지며 알림이 울렸다. 기능 롤아웃을 25% 에서 75% 로 올린 뒤 약 두 시간 만에 늘려 둔 힙마저 바닥난 것이었다."},"updated_at":"2026-11-14T05:53:42.283759Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0214","org_slug":"pagerduty"}