{"id":"IL-0499","org":"Discord","date":"2026-03-25","docs":[],"title":{"en":"Discord voice and video calls hung at Awaiting Endpoint for three hours after 17% of sessions stopped at once","ko":"세션 서버가 대량으로 내려간 뒤 음성 통화 라우팅 서비스가 과부하로 멈춰 음성·영상 통화가 세 시간 넘게 연결되지 않았다"},"domain":["kubernetes","gcp"],"impact":{"scope":"Voice and video calls on Discord: users mostly could not start or join calls; 17% of sessions stopped and gateway connections in us-east1-b were cut","users":"unknown","region":"us-east1-b for sessions and the gateway; voice syncer traffic globally"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Discord says a large enough traffic spike will find a bottleneck somewhere; in earlier large-load incidents its ScyllaDB databases limited recovery, and this time the bottleneck moved to the voice services. Facing a capacity constraint, the team asks whether to increase supply, such as adding services, or reduce demand, such as rate limiting inbound traffic. During an incident it matters to know the toolbox, and Elixir and the BEAM give engineers many introspection options. Any slowdown in the hot loop of a single-threaded process can degrade both the service and the user experience.","ko":"충분히 큰 트래픽 급증은 시스템 어딘가의 병목을 찾아낸다. 이전의 큰 부하 장애에서는 여러 ScyllaDB 데이터베이스가 복구를 막았지만, 이번에는 데이터베이스가 잘 버텼고 병목이 음성 서비스로 옮겨 갔다. 용량 한계를 만나면 공급을 늘리거나 수요를 줄이는 두 길이 있고, Discord 는 장애 대응과 사후 조치를 정할 때 이 질문을 분명히 던진다. 장애 중에는 쓸 수 있는 도구를 알아야 한다. Elixir 와 BEAM 가상 머신에서는 어떤 프로세스의 상태든 꺼내 볼 수 있고 핫패치로 관측을 더할 수도 있다. 분산 시스템에서는 장애 모드를 따져야 한다. Elixir 프로세스는 메시지를 하나씩 처리하므로 바쁜 프로세스의 핵심 루프가 느려지면 서비스와 사용자 경험이 함께 나빠진다. Discord 는 사후 검토에서 장애를 어떻게 피할지뿐 아니라 일어난 뒤 어떻게 더 빨리 풀지도 물었고, 복구 시간을 줄이면 관계없는 장애에서도 더 빨리 회복할 수 있다고 봤다."},"related":[],"sources":[{"url":"https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage","kind":"blog","archived":"https://web.archive.org/web/20261102192515/https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage"}],"summary":{"en":"Discord's Realtime Infrastructure team was moving its stateful Elixir services to Kubernetes and was finishing the migration of the sessions service, which runs one session process for every connected device. Before a host goes offline, its processes must hand their data to another node, and deployments wait until an entity count on the server reaches zero. To lower CPU usage that ran high on weekends, the team deployed a change that gave the session pods more CPU and memory and reduced the pod count in proportion. In the first zone, us-east1-b, Kubernetes terminated 50% of the pods. The service tries to hand off its processes when Kubernetes signals it, but a safety check that waits for in-progress events held the handoff until the termination grace period had passed. Because the service runs in three equally balanced zones, 17% of all sessions stopped without a handoff.\n\nProcess monitors sent a :DOWN message for every stopped session to each process watching it. The gateway service told the affected clients to reconnect, and the start session rate limit, not yet re-tuned after the migration, let more concurrent session starts through because there were more pods than in the old VM setup; the gateway nodes in us-east1-b ran out of memory. As sessions left and rejoined calls, the voice syncers service, which decides where calls run and sends RPC commands to more than 25,000 SFU instances, had to open a flood of outbound HTTPS connections. These go through Holster, an internal library that pools connections from the Erlang HTTP client gun, and every new connection passed through two single-process Erlang supervisors, one for Holster.Pool and one for all gun connections. Each spawn does a selective receive over the supervisor's mailbox, which adds about 1ms with a mailbox queue of about 100k, so the backlog could not be cleared. The etcd connection used for service discovery went through the same supervisors, so voice syncers instances could not refresh their registration and dropped out of etcd when the 60s time-to-live expired.\n\nTargeted restarts and a full cluster restart recovered the instances only briefly. The call owner services' existing limits on voice syncer creation turned out to be outdated or unset, and by 13:43 PDT the calls, streams and guilds services had low syncer spawn limits and the cluster was restarted again. This time the gun supervisor backed up instead of Holster.Pool, the instances stayed announced in etcd and part of the RPCs to SFUs succeeded. Targeted restarts from 14:03 then fully recovered instances, and at 14:15 15 new voice syncers instances, provisioned on GCP VMs through Terraform and Salt and added by hand as ring candidates in etcd, came online and doubled the cluster. The last backed-up instance was restarted at 14:26 and the doubled cluster was healthy.","ko":"Discord 의 Realtime Infrastructure 팀은 Elixir 서비스를 쿠버네티스로 옮기는 중이었고, sessions 서비스의 이전을 마무리하고 있었다. sessions 서비스는 접속한 기기마다 세션 프로세스를 하나씩 두고, 클라이언트로 보내는 모든 것이 세션을 거친다. 호스트를 내릴 때는 그 호스트의 모든 프로세스가 다른 노드로 데이터를 넘긴 뒤에 파드를 종료해야 한다. 팀은 주말 CPU 사용률을 낮추려고 파드를 수직 확장하고 파드 수를 줄이는 변경을 2026년 3월 25일 12:13 PDT에 첫 번째 존에 배포했다. 쿠버네티스가 그 존의 파드 50% 를 종료했고, 핸드오프가 시작되기 전에 종료 유예 시간이 끝나 Discord 전체 세션의 17% 가 갑자기 멈췄다.\n\n세션이 멈추자 process monitor 가 그 세션을 감시하던 모든 프로세스에 {:DOWN, …} 메시지를 보냈다. us-east1-b 의 gateway 는 영향을 받은 사용자에게 재접속을 지시했다. 세션 시작 속도 제한은 마이그레이션 뒤 다시 맞추지 않아 파드 수가 늘어난 만큼 더 많은 요청을 통과시켰고, us-east1-b 의 gateway 노드가 메모리 부족으로 재시작했다. 남은 사용자들의 gateway 연결도 끊겨 다른 존으로 다시 접속했다.\n\n많은 세션이 음성 통화에서 빠졌다가 다시 들어오면서 voice syncers 서비스가 전 세계 25,000대가 넘는 SFU 인스턴스로 HTTPS 연결을 대량으로 만들었다. voice syncers 서비스는 활성 음성 상태를 보고 통화를 어느 SFU 에 둘지 정하고 관리한다. voice syncers 는 내부 라이브러리 Holster 로 Erlang HTTP 클라이언트 gun 의 연결을 풀링한다. 새 연결을 만들 때마다 두 GenServer 프로세스를 거치는데, 하나는 Holster.Pool 의 supervisor 이고 다른 하나는 모든 gun 연결을 맡는 supervisor 다. 이 supervisor 들은 자식 프로세스를 만들 때 selective receive 로 메일박스를 훑으며 응답을 기다리므로, 메일박스가 커질수록 자식 생성이 느려져 끝내 따라잡지 못한다. 장애 뒤 시험에서 메일박스 대기열이 약 10만일 때 프로세스 생성에 약 1ms 가 더 걸렸다. 분산 키-값 저장소인 etcd 는 Elixir 서비스의 서비스 디스커버리를 맡는다. etcd 연결도 같은 supervisor 를 거치므로, 등록 갱신이 막힌 인스턴스는 60초 TTL 이 지나 etcd 에서 빠졌다.\n\n팀은 12:43에 인스턴스 2-13 의 voice syncers 애플리케이션을, 12:47에 인스턴스 2-1 의 Holster.Pool DynamicSupervisor 를 재시작했다. 두 인스턴스 모두 잠깐 회복했지만, 링 밖으로 빠진 비정상 인스턴스의 두 번째, 세 번째 장애 조치 대상으로 트래픽을 더 받으면서 메일박스가 다시 불어났다. 13:05에 클러스터 전체를 재시작했지만 수백만 통화를 한꺼번에 다시 만드는 부하 때문에 13:09에 모든 노드의 메일박스가 다시 늘었다. 통화를 소유하는 서비스의 syncer 생성 속도 제한은 낡았거나 설정돼 있지 않았다. guilds 의 제한은 길드마다 coordinator syncer 하나만 제한했고, 음성 채널마다 만드는 자식 syncer 는 제한하지 않았다. 13:43에 calls, streams, guilds 서비스에 낮은 syncer 생성 제한을 넣고 다시 재시작하자, 막히는 supervisor 가 gun supervisor 쪽으로 바뀌어 인스턴스들이 etcd 등록을 유지하고 일부 RPC 를 보냈다.\n\n14:03에 인스턴스 2-3 재시작이 완전히 성공했고, 14:15까지 다섯 대를 재시작해 네 대가 완전히 회복했다. 같은 때 Salt 로 준비한 인스턴스 15대가 추가돼 용량이 두 배가 됐고 인스턴스당 syncer 수가 절반이 됐다. 14:26에 마지막으로 막혀 있던 인스턴스를 재시작해 두 배로 늘어난 클러스터가 완전히 정상이 됐다."},"trigger":{"en":"A routine change to tune the sessions service cluster on Kubernetes gave each pod more CPU and memory and lowered the pod count in proportion. When it was deployed to the first zone at 12:13 PDT, Kubernetes terminated 50% of that zone's pods, and the termination grace period ran out before the sessions could be handed off.","ko":"Elixir 서비스를 쿠버네티스로 옮기던 중, 주말 CPU 사용률을 낮추려고 sessions 서비스 파드의 CPU 와 메모리를 늘리고 파드 수를 그만큼 줄이는 변경을 배포했다. 레플리카 수가 줄자 쿠버네티스가 첫 번째 존의 파드 50% 를 종료했고, 진행 중인 다른 작업을 기다리는 안전 검사 때문에 핸드오프가 시작되기 전에 종료 유예 시간이 끝났다."},"severity":"unknown","symptoms":["service-unavailable"],"timeline":{"detected_after":null,"resolved_after":"3h 17m"},"date_kind":"incident","detection":null,"components":["pod-termination","coordination-store","database-proxy"],"root_cause":"config-change","remediation":[{"text":{"en":"Set low syncer spawn rate limits in the calls, streams and guilds services and restarted the voice syncers cluster","ko":"13:43 PDT까지 calls, streams, guilds 서비스에 낮은 syncer 생성 제한을 넣고 voice syncers 클러스터를 다시 재시작했다. 막히는 supervisor 가 gun supervisor 로 바뀌면서 인스턴스들이 etcd 등록을 유지했고 일부 syncer 의 RPC 를 SFU 로 보냈다."}},{"text":{"en":"Restarted voice syncers instances one at a time, and four of the first five recovered fully","ko":"인스턴스를 골라 하나씩 다시 재시작했고, 14:03에 재시작한 인스턴스 2-3 처럼 일부 인스턴스가 완전히 회복했다."}},{"text":{"en":"Brought 15 new voice syncers instances online, doubling capacity and halving the syncers per instance","ko":"인스턴스 15대를 추가로 올려 voice syncers 클러스터의 용량을 두 배로 늘렸다. 새 인스턴스들은 메일박스 대기열 문제 없이 syncer 를 받았고, 인스턴스당 syncer 수가 절반이 됐다."}},{"text":{"en":"Restarted the last backed-up voice syncers instance, leaving the doubled cluster fully healthy","ko":"14:26에 마지막으로 막혀 있던 인스턴스를 재시작해 두 배로 늘어난 클러스터가 완전히 정상이 됐다."}}],"action_items":[{"text":{"en":"Add a validating admission webhook that rejects a scale-down of Elixir workloads until each pod has handed off its processes","ko":"Elixir 워크로드용 validating admission webhook 을 쿠버네티스 클러스터에 넣어, 파드가 프로세스를 다 넘기기 전에는 워크로드를 줄이는 업데이트를 거부한다."}},{"text":{"en":"Replace the Holster.Pool supervisor with a PartitionSupervisor so outgoing connections are created and checked out concurrently","ko":"부하 테스트 뒤 Holster.Pool 의 supervisor 를 PartitionSupervisor 로 바꿔, 연결 풀 요청을 독립된 여러 supervisor 에 나누고 나가는 연결을 동시에 만들거나 꺼낼 수 있게 했다."}},{"text":{"en":"Move gun connection process lifecycle management into Holster.Pool so the gun connection supervisor is no longer used","ko":"gun 연결 프로세스의 생명주기 관리를 그 프로세스를 만드는 Holster.Pool 로 옮겨 gun 연결 supervisor 를 아예 쓰지 않게 했다."}},{"text":{"en":"Tune the upstream rate limits that call into voice syncers, with load shedding tooling that can force them","ko":"voice syncers 를 호출하는 상위 서비스의 속도 제한을 다시 조정해 더 정확히 걸리게 했다. 필요하면 로드 셰딩 도구로 이 제한을 강제로 적용해 voice syncer 를 다시 만들게 할 수도 있다."}},{"text":{"en":"Add rate limits on endpoint selection in voice syncers to limit outbound connection creation","ko":"voice syncers 의 엔드포인트 선택에 새 속도 제한을 넣고 조정해, 나가는 연결 생성을 제한하는 수단을 하나 더 뒀다."}},{"text":{"en":"Monitor the HTTP connection pooling mailboxes","ko":"HTTP 연결 풀링 메일박스에 감시를 더해 언제 문제가 되고 언제 아닌지 더 분명히 알 수 있게 했다."}},{"text":{"en":"Review and expand instrumentation and logging for service discovery announcement and voice syncer RPCs to SFUs","ko":"서비스 디스커버리 등록과 voice syncer 에서 SFU 로 가는 RPC 트래픽의 계측과 로그를 전부 검토하고 넓혔다."}},{"text":{"en":"Monitor the most critical processes across all services instead of aggregate metrics such as overall mailbox health","ko":"메일박스 상태 합계 같은 간접 지표 대신, 모든 서비스에서 가장 중요한 프로세스를 직접 감시하도록 넓혀 간다."}},{"text":{"en":"Bring more workloads into Kubernetes so capacity can be scaled with a single command instead of provisioning VMs by hand","ko":"더 많은 워크로드를 쿠버네티스로 옮겨, VM 을 손으로 준비하는 대신 명령 하나로 파드를 늘릴 수 있게 한다."}},{"text":{"en":"Re-architect the A/V system to scale its control plane services horizontally across multiple regions","ko":"A/V 인프라의 컨트롤 플레인 서비스를 여러 리전에 수평 확장해 신뢰성과 리전 장애 내성을 얻고, 나가는 연결 급증을 전 세계에 고르게 흩는다."}}],"contributing":["retry-storm","limit-unknown"],"recurrence_of":null,"symptoms_text":{"en":"From 12:13 to 15:30 PDT on 25 March 2026, voice and video on Discord were heavily degraded. Users mostly could not start or join calls and saw \"Awaiting Endpoint\" in their call status.\n\nIt started when a deploy of the sessions service on Kubernetes stopped 17% of all Discord sessions without handing them off. The gateway service, which holds the websocket connections, told the affected clients to reconnect, and the flood of session starts made memory spike on all gateway nodes in us-east1-b until they ran out of memory and restarted. That cut the gateway connections of the remaining users in us-east1-b, who reconnected again through us-east1-c and us-east1-d.\n\nFrom 12:13 PDT the aggregate process mailbox length on all 15 voice syncers instances grew and RPCs from voice syncers to the SFUs fell sharply. The instances dropped out of the etcd-based consistent hash ring, stopped taking on new syncers and could not make RPC requests. A snapshot of the running Erlang processes taken with Recon, sorted by mailbox queue length, put the Holster pool's DynamicSupervisor first.\n\nThe first recovery attempts did not last. Restarting the voice syncers application on instance 2-13 at 12:43 PDT and restarting the Holster.Pool DynamicSupervisor on 2-1 at 12:47 brought each instance back only briefly, because the few healthy instances also took the traffic of the unhealthy ones as secondary and tertiary placements and their mailboxes grew again. A restart of the whole voice syncers cluster at 13:05 recreated millions of active calls at once, and by 13:09 all restarted nodes had growing mailboxes again.","ko":"2026년 3월 25일 12:13 PDT부터 15:30 PDT까지 Discord 의 음성·영상 기능이 크게 망가졌다. 사용자 대부분이 통화를 시작하거나 참여하지 못했고, 통화 상태에는 “Awaiting Endpoint” 메시지가 떴다.\n\n12:13 PDT에 설정 변경이 첫 번째 존에 배포되자 쿠버네티스가 그 존의 sessions 서비스 파드 50% 를 종료했다. 프로세스 핸드오프가 시작되기 전에 쿠버네티스의 종료 유예 시간이 끝났고, 세 존에 고르게 나뉜 세션 가운데 17% 가 정상 종료 절차 없이 멈췄다.\n\n세션이 멈추자 그 세션을 감시하던 프로세스마다 {:DOWN, …} 메시지가 쏟아졌다. us-east1-b 의 gateway 프로세스들이 이 메시지를 받고 클라이언트에 재접속을 지시했다. 세션 시작 속도 제한은 마이그레이션 뒤 다시 조정하지 않은 상태여서, 예전 VM 구성보다 늘어난 파드 수만큼 더 많은 세션 시작을 통과시켰다. us-east1-b 의 모든 gateway 노드에서 메모리 사용량이 치솟아 메모리가 바닥났고 노드가 재시작했다. 이 때문에 us-east1-b 에 남아 있던 사용자의 gateway 연결도 끊겼고, 사용자들은 us-east1-c 와 d 로 다시 접속했다.\n\n같은 12:13부터 voice syncers 인스턴스 15대 전체의 프로세스 메일박스 길이 합계가 늘기 시작했고, 통화 참가자 사이의 종단 간 암호화 미디어를 중계하는 SFU 로 가는 RPC 가 급감했다. 인스턴스들은 etcd 기반의 일관성 해시 링에서 빠졌고, 새 syncer 를 받지 않았으며, RPC 요청을 보내지 못했다. etcd 를 조회하자 voice syncers 인스턴스 대부분이 등록하지 않은 채 링 밖에 있었다. Elixir 와 Erlang 운영 시스템의 문제를 진단하는 도구인 Recon 으로 실행 중인 Erlang 프로세스를 메일박스 대기열 길이 순으로 정렬하자 Holster.Pool 의 DynamicSupervisor 가 맨 위에 있었다.\n\n인스턴스 2-8 하나만 메일박스를 비우고 etcd 에 다시 등록했고, 이 때문에 엔드포인트 선택과 RPC 생성 성공이 잠깐 치솟았다. 팀은 인스턴스를 하나씩 재시작하고, Holster.Pool DynamicSupervisor 만 따로 재시작하고, 클러스터 전체를 재시작해 봤다. 그때마다 인스턴스가 잠깐 회복했다가 메일박스가 다시 불어났다."},"updated_at":"2026-11-14T12:34:14.434590Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0499","org_slug":"discord"}