[post_pointcloud_state_change_worker] Retrying 1 times, error: 429 Too Many Requests
RCA: post_pointcloud_state_change_worker 429 Too Many Requests
Overview#
What Happened#
2026-06-13 18:01 KST 무렵, cupixworks-worker의 PostPointcloudStateChangeWorker가 Slack incoming webhook(hooks.slack.com)에 알림을 전송하다 4초 동안 13건의 429 Too Many Requests를 기록했다. 모든 job 은 Sidekiq 의 1차 retry 에서 성공했고, Final retry attempt failed 로그는 발생하지 않아 사용자 영향은 없었다 (Slack 메시지 일부의 도착 지연만 발생).
Quick Facts#
| Field | Value |
|---|---|
| exception.class | RestClient::TooManyRequests (RestClient::Exception, http_code 429) |
| exception.message | 429 Too Many Requests |
| top_frame | app/workers/post_pointcloud_state_change_worker.rb:24 (Cupix::HttpClient.post) |
| runtime | Ruby on Rails (Sidekiq worker) |
| env | production, region us-west-2 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| Pointcloud notifications (Slack #pointcloud-state) | 13 | Slack 알림 도착이 1차 Sidekiq retry 만큼 지연. 처리 파이프라인 자체 영향 없음 |
Timeline#
- 2026-06-13 18:01:49 KST — Slack webhook 첫 429 응답 (3건 기록)
- 2026-06-13 18:01:51 KST — 추가 4건 429
- 2026-06-13 18:01:53 KST — 마지막 6건 429 (총 13건)
- 이후 — Sidekiq job-level 1차 retry 가 모두 성공,
Retrying 2 times또는Final retry attempt failed로그 없음
Error Log#
[post_pointcloud_state_change_worker] Retrying 1 times, error: 429 Too Many Requests
Impact#
- Service:
cupixworks-worker - 발생 횟수: 13
- 최초 발생: 2026-06-13 18:01:49 KST
- 최근 발생: 2026-06-13 18:01:53 KST
- 사용자 영향: 없음 (Sidekiq retry 로 모두 복구). 다만 동시에 처리되는 pointcloud 가 많을 때 Slack 알림 지연 가능.
Root Cause Summary#
Slack incoming webhook 은 공식적으로 webhook 당 1 message/sec (short bursts >1 허용) rate limit 을 가진다 (Slack Web API rate limits — Incoming webhooks: "1 per second", "Short bursts >1 allowed"). 메시지 게시(chat.postMessage 및 incoming webhook 포함) 는 추가로 채널 당 1 msg/sec 의 per-channel 제한과 workspace-wide ceiling 도 함께 적용된다 (같은 문서). 이 워커는 SLACK_SERVICE_WEBHOOK_URL 단일 webhook 으로 단일 채널(#pointcloud-state) 에 게시하므로, webhook-level / channel-level 두 limit 모두 같은 1 msg/sec 로 수렴한다. 다수의 pointcloud 가 동시에 상태 전이(queued / processing / done)하는 burst 가 발생하면 Notifiable::Pointcloud#notify 가 동일 webhook 으로 메시지를 짧은 시간에 다발적으로 enqueue 한다. Cupix::HttpClient.post 가 client-side 에서 최대 3회 (1s/2s/4s + jitter) 재시도하지만 burst 길이가 이를 초과하면 결국 RestClient::Exception(http_code 429) 이 worker 까지 전파되고, Sidekiq 의 sidekiq_retry_in 훅이 Retrying 1 times, error: 429 Too Many Requests 를 error 레벨로 로깅한다. job 은 Sidekiq 기본 backoff 로 다시 실행되어 결국 성공하므로 기능적 장애는 아니지만, 정상 운영 중 발생하는 transient rate-limit 이 error 레벨로 alert noise 를 만든다.
Technical Analysis#
Code Path#
- Trigger:
app/models/concerns/notifiable/pointcloud.rb:48— pointcloud 상태 전이 시마다 worker enqueue - Worker entry:
app/workers/post_pointcloud_state_change_worker.rb:13(perform) - HTTP call:
app/workers/post_pointcloud_state_change_worker.rb:24(Cupix::HttpClient.post) - Client retry:
lib/cupix/http_client.rb:34-46— 3회 재시도 (429/502/503/504) - Failure point (retry 훅):
app/workers/post_pointcloud_state_change_worker.rb:5-7(sidekiq_retry_in블록에서 error 레벨 로그)
def notify
return unless self.pointcloud_group?
case state
when 'done'
description = 'Processing completed'
icon = ':clap:'
when 'queued'
description = 'Waiting in queue'
icon = ':hourglass_flowing_sand:'
when 'processing'
description = 'Processing started'
icon = ':rocket:'
when 'error'
description = "Error (error_code: #{self.error_code})"
icon = ':exclamation:'
else
return
end
message = "#{icon} [#{id}] <#{team_url}/pj/#{facility.key}/cap/#{record_id}/pc?cplv=#{level_id}|*#{name}* \| _#{facility.name}_ \| _#{team.name}_> #{description}"
PostPointcloudStateChangeWorker.perform_async(message)
end
class PostPointcloudStateChangeWorker
include Sidekiq::Worker
sidekiq_options queue: :default, retry: 2
sidekiq_retry_in do |count, e|
Cupix::Logger.error("[post_pointcloud_state_change_worker] Retrying #{count + 1} times, error: #{e.message}")
end
sidekiq_retries_exhausted do |job, e|
Cupix.logger.error "[post_pointcloud_state_change_worker] Final retry attempt failed: #{job['args'].first}, error: #{e.message}"
end
def perform(message)
channel = 'pointcloud-state'
channel = "pointcloud-state-#{Rails.env}" unless Rails.env.production?
data = {
channel: channel,
username: $SLACK_REGION_USER_NAME,
icon_emoji: SLACK_REGION_ICON,
text: message
}
Cupix::HttpClient.post(SLACK_SERVICE_WEBHOOK_URL, data.to_json, { content_type: :json })
Cupix::Logger.info("[post_pointcloud_state_change_worker] Successfully sent message to webhook: #{message}")
end
end
def self.post(url, payload, headers = {}, retries: MAX_RETRIES)
attempt = 0
begin
RestClient.post(url, payload, headers)
rescue RestClient::Exception => e
if RETRIABLE_STATUS_CODES.include?(e.http_code) && attempt < retries
attempt += 1
sleep((2**(attempt - 1)) + rand(0.0..0.5))
retry
end
raise
end
end
기대 동작: client-side 재시도(MAX_RETRIES = 3, 최대 7s + jitter) 안에서 Slack 이 회복되어 200 응답을 돌려주면 worker 는 정상 종료한다.
실제 동작: 동시에 다수의 pointcloud sub-cluster 가 상태 전이하는 burst (Datadog info 로그에서 단일 capture 의 Sub-Cluster 17 이 같은 초에 queued/done 상태로 메시지 전송되는 패턴 확인됨) 에서 7초 안에 회복되지 않아 13개 job 이 worker 까지 예외를 전파했다. sidekiq_retry_in 블록은 retry 간격 계산 대신 로깅 용도로만 쓰이고 (반환값이 numeric 이 아니므로 Sidekiq 기본 exponential backoff 사용), Sidekiq job-level 1차 retry 시 모두 성공했다.
Log Evidence#
Datadog query (cluster 파일에서 그대로):
service:cupixworks-worker status:error @environment:production "[post_pointcloud_state_change_worker] Retrying 1 times, error: 429 Too Many Requests"
13건 모두 동일 메시지. 시간 분포(KST):
2026-06-13 18:01:49 ×3
2026-06-13 18:01:51 ×4
2026-06-13 18:01:53 ×6
같은 시간대(service:cupixworks-worker "post_pointcloud_state_change_worker" info) 에서 burst 패턴 확인:
2026-06-13 18:25:20 info [...] :hourglass_flowing_sand: [1146653] ... Sub-Cluster 1 ... Waiting in queue
2026-06-13 18:25:14 info [...] :hourglass_flowing_sand: [1146649] ... Sub-Cluster 1 ... Waiting in queue
2026-06-13 18:25:10 info [...] :hourglass_flowing_sand: [1146651] ... Sub-Cluster 2 ... Waiting in queue
2026-06-13 18:25:00 info [...] :hourglass_flowing_sand: [1146647] ... Sub-Cluster 2 ... Waiting in queue
2026-06-13 18:24:50 info [...] :hourglass_flowing_sand: [1146645] ... Sub-Cluster 3 ... Waiting in queue
2026-06-13 18:24:38 info [...] :hourglass_flowing_sand: [1146643] ... Sub-Cluster 5 ... Waiting in queue
2026-06-13 18:24:24 info [...] :hourglass_flowing_sand: [1146641] ... Sub-Cluster 4 ... Waiting in queue
2026-06-13 18:24:16 info [...] :hourglass_flowing_sand: [1146639] ... Sub-Cluster 3 ... Waiting in queue
→ 단일 capture(132781)의 7개 sub-cluster 가 같은 분 안에 queued 와 done 양쪽 메시지를 발행하는 패턴. 동일 webhook 으로 short-burst 알림이 집중됨이 확인된다.
Retrying 2 times 검색 결과:
service:cupixworks-worker "post_pointcloud_state_change_worker" "Retrying 2 times"
→ 0 logs
Final retry 검색 결과:
service:cupixworks-worker "post_pointcloud_state_change_worker" "Final retry"
→ 0 logs
→ 모든 job 이 Sidekiq 1차 retry 에서 복구됨.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Slack incoming webhook rate limit (공식: webhook 당 1 msg/sec, 채널 당 1 msg/sec, short bursts 허용 — docs) 을 burst 알림이 초과 | 동일 webhook(SLACK_SERVICE_WEBHOOK_URL) 으로 다수 worker 가 동시에 POST; 같은 capture 의 7개 sub-cluster 가 같은 분에 queued/done 두 번씩 발행되는 info 로그; 13건이 4초 안에 집중; client retry(lib/cupix/http_client.rb:34-46)는 7s 한도 안에서 회복 실패 |
— | Confirmed |
| H2 | Slack 측 광역 incident / outage | 동일 시간대 다른 worker(post_record_state_change_worker, post_transfer_state_change_worker 등) 도 동일 webhook 사용 |
다른 webhook 사용 worker 들의 429 에러가 동시 발생했다는 evidence 없음; 4초 만에 자체 회복; Slack status 페이지 incident 와의 상관관계 미확인 | Rejected |
| H3 | Worker 코드 결함 (예: payload 손상) → Slack 가 거부 | — | 메시지 payload 가 정상이라면 retry 시 또 429 가 떠야 함. 그러나 Sidekiq 1차 retry 에서 모두 성공 → payload 문제 아님 | Rejected |
| H4 | Cupix::HttpClient 의 retry 로직 자체 결함 |
— | lib/cupix/http_client.rb:34-46 코드상 429 에 대해 정상적으로 재시도; sleep + jitter 도 적절. 단지 7s 안에 회복 안 된 것뿐 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
없음. 사용자 영향이 없고(Sidekiq retry 로 모두 성공), error 레벨 alert noise 만 발생.
단기 개선 (1주 이내)#
app/workers/post_pointcloud_state_change_worker.rb:6의sidekiq_retry_in훅 로그 레벨을 검토. transient 한 429/502/503/504 에 대한 1차 Sidekiq retry 는warn레벨이 적절.Final retry attempt failed(sidekiq_retries_exhausted, line 10) 만error로 유지하면 진짜 장애 시점에만 alert 가 트리거된다. (메모리에 기록된 AUTH20022/AUTH20023 사례와 동일한 패턴: 정상 운영에서 발생 가능한 transient 조건은warn)- 같은 패턴이 다른 Slack-webhook worker(
post_record_state_change_worker,post_transfer_state_change_worker,post_deviation_state_change_worker,refresh_elements_count_worker,publish_siteinsights_events_*) 에도 존재 — 동일 처리를 일괄 적용하거나 공통 base worker 로 정리 검토.
장기 개선 (재발 방지)#
- 동일 webhook 으로의 burst 를 줄이기 위한 옵션:
- sub-cluster 단위가 아닌 cluster 단위로 알림을 집계 (예: 같은
record_id의 sub-cluster 상태를 1초 debounce 후 단일 메시지로 발송) - Slack 알림 worker 를 별도 큐(
queue: :slack_notifications) 와 단일 concurrency 의 Sidekiq 프로세스로 분리하여 자연스럽게 직렬화
- sub-cluster 단위가 아닌 cluster 단위로 알림을 집계 (예: 같은
Cupix::HttpClient의MAX_RETRIES또는 backoff 상한을 webhook 호출에 한해 늘리는 것은 Sidekiq thread pool 점유 시간을 늘리므로 권장하지 않음.
Monitoring#
이미 있는 service:cupixworks-worker status:error "[post_pointcloud_state_change_worker]" 검색이 충분. 추가 monitoring 권장:
전체 retry 발생률(transient 포함):
service:cupixworks-worker @environment:production "[post_pointcloud_state_change_worker] Retrying"
진짜 장애만 (alert 대상):
service:cupixworks-worker status:error @environment:production "[post_pointcloud_state_change_worker] Final retry attempt failed"
다른 Slack-webhook worker 의 전체 429 동향:
service:cupixworks-worker @environment:production "Retrying" "429 Too Many Requests"
Risk Assessment#
- Risk level: low (transient, 사용자 영향 없음, Sidekiq retry 로 자체 회복)
- 예상 복잡도: trivial (로그 레벨 조정만 적용 시) / standard (debounce/batching 까지 진행 시)
Revision History#
Revision 1#
Feedback: "slack webhook rate limit 이 채널/webhook 당 얼마인지 공식문서 찾아바"
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| Slack incoming webhook 의 채널/webhook 당 rate limit 을 공식문서로 확인 | 수용 | Slack 공식 문서 docs.slack.dev/apis/web-api/rate-limits 의 Incoming Webhooks 항목: "1 per second", "Short bursts >1 allowed" (per webhook). 동일 문서의 Posting messages 항목: 메시지 게시(chat.postMessage 및 incoming webhook 포함) 는 채널 당 1 msg/sec 와 workspace-wide ceiling 도 적용. 본 incident 의 webhook(SLACK_SERVICE_WEBHOOK_URL) 은 단일 channel(#pointcloud-state) 로만 게시하므로 두 limit 이 동일하게 1 msg/sec 로 수렴함. 기존 보고서의 "≈1 msg/sec" 추정은 정확했으나 출처 미기재 상태였다. |
변경 사항:
## Root Cause Summary— "약 1 msg/sec" 추정 표현을 공식 수치(webhook 당 1 msg/sec, 채널 당 1 msg/sec, short bursts 허용) 로 교체하고 출처 링크 추가. 본 incident 가 webhook-level / channel-level 어느 limit 에도 동일하게 부합함을 명시.## Hypotheses ConsideredH1 행에 공식 출처 링크 추가.
추가 조사 내용:
https://docs.slack.dev/apis/web-api/rate-limits(Slack Web API rate limits) — Incoming Webhooks tier 와 Posting messages tier 의 정확한 문구 확인.https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks도 확인했으나 rate limit 구체 수치는 기재되어 있지 않음 (rate-limits 페이지로 위임).