← 전체 글 보기

Discord UI 갱신이 왜 장애가 되었나: 응답 제약과 Rate Limit를 동시에 다루는 설계

Discord 기반 상호작용 시스템에서 장애는 보통 단일 API 실패로 시작되지 않는다. 응답 속도를 높이기 위해 이벤트를 잘게 쪼개고 UI를 자주 갱신하는 설계가 누적되면, 호출 총량과 상태 전이 복잡도가 함께 증가한다. 이 글은 그 누적이 어떻게 Rate Limit 리스크로 수렴했는지, 그리고 이를 제어하기 위해 어떤 구조적 분리를 적용했는지 기술적으로 정리한다.

장애 신호를 하나의 문제로 묶기

초기에는 아래 현상이 서로 다른 버그처럼 보였다.

하지만 운영 로그를 시간축으로 정렬하면 공통 축이 명확하다.

  1. 상호작용 이벤트 1건당 메시지 발행 횟수 증가
  2. 발행 경로 내부에서 조회/검증/렌더링 로직이 중첩
  3. ACK 처리와 후속 작업이 같은 임계 구간에서 경쟁

결론적으로 문제의 본질은 “응답 경로의 과부하 + 상태 전이 결합"이다.

실패 메커니즘

1) 전송 경로 결합

기존 경로는 다음과 같은 형태였다.

interaction -> validation -> read(DB/KV) -> render -> send -> follow-up

핵심 문제는 send 이전에 변동성이 큰 작업이 과도하게 들어간다는 점이다. 이 구조에서는 한 단계 지연이 전체 응답 지연으로 전파되고, 재시도 시 동일 경로가 중복 실행된다.

2) 메시지 발행 정책 부재

요청 단위를 정의하지 않고 “보여줄 수 있는 상태를 즉시 노출"하는 정책을 택하면, 동일한 도메인 이벤트가 다수의 UI 메시지로 분해된다.

3) 재시도 비결정성

Idempotency 키 없이 재시도하면 다음 문제가 발생한다.

구조 개편: 응답 경로를 2단계로 분리

핵심 변경은 “빠른 ACK 경로"와 “비동기 후속 경로"를 분리하는 것이다.

1) Fast Path (동기)

interaction -> auth/shape validation -> ack

2) Async Path (비동기)

event queue -> state projection -> render -> follow-up send

전송 정책 설계

메시지 발행을 기능 단위가 아닌 정책 단위로 제한했다.

핵심은 “표현 가능한 변화"와 “전송 가능한 변화"를 분리하는 것이다.

상태/재시도 안정화

Idempotency 키

아래 조합으로 키를 고정했다.

<platform_event_id>:<route_version>:<intent_type>

재시도 시 동일 키가 감지되면 전송/후처리를 스킵하거나 병합한다.

상태 전이 규칙

관측 지표

리스크를 제어하려면 호출량보다 경계 지표를 먼저 본다.

특히 events_per_interactionmessages_per_interaction의 괴리가 커지면, 설계상 불필요한 표현 계층이 증가한 신호다.

트레이드오프

다음 단계

  1. 인터랙션 타입별 전송 예산(메시지/조회 횟수) 강제
  2. 비동기 경로의 순서 보장 필요 구간만 직렬화
  3. 지표 기반 자동 완화(발행 상한 초과 시 강제 집계/샘플링)

참고 및 인용

참고: Discord는 글로벌/라우트 단위 Rate Limit를 문서화하고, 헤더 기반 제어를 권장한다. Rate Limits

참고: Interaction 응답 제약(초기 응답 + 후속 응답)을 분리하는 구조는 타임아웃 리스크를 줄인다. Receiving and Responding

참고: 재시도 폭주를 줄이기 위한 jitter/backoff 패턴은 대규모 API 운영의 기본 원칙이다. Timeouts, retries, and backoff with jitter