ES /docs

Cupix::HttpClient missing timeout configuration — downstream latency propagation

RCA: Api::V1::IssueTypesController#index avg 19142ms (latency spike)

Overview#

What Happened#

2026-07-18 03:45 KST 시점에 cupixworks-api (region ap-southeast-2) 의 Api::V1::IssueTypesController#index 요청 1건이 19142ms 만에 응답했다. 요청 자체는 HTTP 200 으로 성공했지만, 정상 응답 시간(수십~수백 ms) 대비 매우 느려 latency 클러스터로 감지되었다. 동일 시간대에 ap-southeast-2 리전의 API 전체 평균 latency 가 0.1s → 12–17s 로 급등하는 리전 광역 slowdown 이 관측되어, 단일 endpoint 결함이 아닌 리전/업스트림 이슈로 판단된다.

Quick Facts#

Field Value
resource_name Api::V1::IssueTypesController#index
avg_duration_ms 19142
max_duration_ms 19142
sample_trace_id 607179151302061438
cluster_type latency
env production, region ap-southeast-2
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (ap-southeast-2) 1 (trace) 사용자 요청 1건이 19s 지연되어 클라이언트 UX 저하. 동일 시간대 다른 endpoint 도 함께 지연 (region-wide latency spike)

Timeline#

  1. 2026-07-18 03:21 KST — 관련 인시던트 2026-07-17-svc-cupixworks-api--unknown-3 시작 (sibling cluster a54e2505 — Kinesis TCP timeout us-west-2, 별도 root cause).
  2. 2026-07-18 03:45 KST — Trace 607179151302061438 시작, IssueTypesController#index 호출.
  3. 2026-07-18 03:46:15 KSTCupix::IssueService#list 에서 fetching list from issue service successful. info 로그 (retry 없이 단일 요청 성공).
  4. 2026-07-18 03:46:17 KST — Rails 최종 응답: [200] GET /api/v1/issue_types (총 19142ms 소요).
  5. 2026-07-18 03:45~03:55 KST — ap-southeast-2 리전 API avg latency 12s ~ 17s 지속.

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::IssueTypesController#index",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 19142,
  "max_ms": 19142,
  "sample_trace_id": "607179151302061438"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-18 03:45 KST
  • 최근 발생: 2026-07-18 03:45 KST

Root Cause Summary#

Api::V1::IssueTypesController#index 는 요청 본문 처리 없이 상류 Cupix::IssueService ($CUPIX_ISSUE_SERVICE_URL/issue_types) 로 HTTP GET 하나만 수행하는 얇은 pass-through 컨트롤러다. 문제의 trace 는 retry 없이 upstream 응답을 그대로 대기하다 19142ms 만에 200 을 받았고, 같은 시간대 ap-southeast-2 리전의 API 평균 latency 가 광범위하게 12–17s 로 튀는 광역 slowdown 이 관측된다. 즉, 이 latency 는 controller 로직 결함이 아니라 (a) upstream issue-service 또는 (b) 리전 네트워크/인프라의 응답 지연을 그대로 흡수한 결과다. Cupix::HttpClient.get 이 RestClient 에 명시적 timeout / open_timeout 을 지정하지 않아 하위 서비스가 느려질 때 요청 스레드가 무제한 blocking 되는 코드 취약점이 이 상황을 증폭시켰다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/issue_types_controller.rb:4
  • 실제 upstream 호출: app/services/cupix/issue_service.rb:82-107Cupix::HttpClient.get
  • Failure point (증폭 지점): lib/cupix/http_client.rb:12-24 — timeout 미설정으로 upstream slowdown 을 그대로 전파

Controller 는 매우 얇다. Upstream 응답 시간이 곧 이 endpoint 의 latency 가 된다.

app/controllers/api/v1/issue_types_controller.rb:3-11ruby
class Api::V1::IssueTypesController < Api::V1::ApiController
  def index
    url = "#{$CUPIX_ISSUE_SERVICE_URL}/issue_types"
    resp = Cupix::IssueService.list(url, params)

    raise Cupix::Errors::System.new(code: 'SYS20000', reason: 'Temporarly failed to fetch issue types') if resp.nil?

    render_json resp.code, JSON.parse(resp.body)['result']
  end

Cupix::IssueService.list 는 예외를 rescue 해 nil 로 반환하지만, 정상 응답(200) 은 그대로 반환하므로 upstream 이 느리면 그 시간만큼 여기서 blocking 된다.

app/services/cupix/issue_service.rb:82-107ruby
def list(url, params)
  unless url.start_with?('http')
    Cupix::Logger.info('Bypass posting issue because ISSUE_SERVICE_URL is not set', class: self.name, function: __method__)
    return
  end

  begin
    resp = Cupix::HttpClient.get(
      url,
      {
        params: params.as_json,
        content_type: :json,
        accept: :json,
        authorization: "Bearer #{_access_token}"
      }
    )
  rescue RestClient::Exception => e
    Cupix::Logger.error("fail to fetch list from issue service. reason: 'RestClient error' message: #{e.response}", class: self.name, function: __method__)
  rescue StandardError => e
    Cupix::Logger.error("fail to fetch list from issue service. reason: 'Standard error' message: #{e.message}", class: self.name, function: __method__)
  else
    Cupix::Logger.info('fetching list from issue service successful.', class: self.name, function: __method__)
  end

  resp
end

Cupix::HttpClient.get 은 429/502/503/504 만 재시도한다. 200 응답이 늦게 오면 재시도 자체가 발생하지 않으며, RestClient.get 에 timeout 관련 옵션이 없어 upstream 이 느리면 요청 스레드는 무제한 대기한다.

lib/cupix/http_client.rb:8-24ruby
RETRIABLE_STATUS_CODES = [429, 502, 503, 504].freeze
MAX_RETRIES = 3

sig { params(url: String, headers: T::Hash[T.untyped, T.untyped], retries: Integer).returns(RestClient::Response) }
def self.get(url, headers = {}, retries: MAX_RETRIES)
  attempt = 0
  begin
    RestClient.get(url, 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

기대 동작 vs 실제 동작

  • 기대: upstream 이 수 초 이상 응답하지 못하면 timeout → 500/504 반환하거나 재시도.
  • 실제: timeout 없이 19s 동안 스레드 blocking → 200 을 그대로 클라이언트에 전달. 사용자 관점에서는 페이지 전체가 20s 가까이 멎어 있는 것처럼 보인다.

Log Evidence#

Trace ID 로 조회한 결과, 문제 요청은 정확히 두 개의 로그만 남겼다 (retry 로그 없음, error/warn 로그 없음). 즉, 상류 요청은 재시도 없이 단 한 번의 GET 이 ~19s 동안 열려 있었다.

text
service:cupixworks-api trace_id:607179151302061438
json
[
  {
    "timestamp": "2026-07-18 03:46:15 KST",
    "status": "info",
    "message": "fetching list from issue service successful.",
    "class": "Cupix::IssueService",
    "function": "list"
  },
  {
    "timestamp": "2026-07-18 03:46:17 KST",
    "status": "info",
    "message": "[200] GET /api/v1/issue_types (Api::V1::IssueTypesController#index)"
  }
]

같은 30분 창에 cupixworks-apistatus:error 로그는 0건, Cupix::IssueService 에러 로그도 0건이다.

text
service:cupixworks-api status:error   (2026-07-17T18:20 ~ 19:00 UTC)  → 0 logs
service:cupix-issue-service           (2026-07-17T18:30 ~ 19:00 UTC)  → 0 logs (해당 service 태그 미존재)

APM 메트릭은 이 시점 ap-southeast-2 리전 광역 slowdown 을 뒷받침한다.

text
avg:trace.rack.request.duration{service:cupixworks-api,region:ap-southeast-2}   window=3h
text
정상 구간:  0.04s ~ 0.12s
악화 구간:  1.67s → 2.46s → 3.37s → 4.35s → 5.51s → 6.35s
피크 구간:  12.20s, 17.63s, 12.42s, 13.06s, 13.54s

리전 전체 요청의 평균이 두 자릿수 초로 튀는 것은 특정 endpoint (issue_types) 만의 문제가 아니라 리전 인프라 또는 downstream 공통 자원 (issue-service, DB, VPC 네트워크) 지연을 시사한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Upstream issue-service (또는 리전 인프라) 응답 지연을 controller 가 timeout 없이 대기하여 그대로 흡수 trace 로그 2건 (retry 없음, 200 성공), same window 에서 ap-southeast-2 API avg latency 12–17s 급등, Cupix::HttpClient.get (lib/cupix/http_client.rb:12-24) 에 timeout 옵션 부재 Confirmed
H2 Cupix::HttpClient.get 의 exponential backoff retry 로 인해 지연 (sleep(2^n) + jitter, 최대 ~7s+) 코드상 3회 재시도 시 이론적으로 수 초 발생 가능 trace_id 로그에 retry 흔적 없음(성공 로그 1건뿐), RETRIABLE_STATUS_CODES 는 [429,502,503,504] 뿐인데 결과는 200 → retry 조건 미충족 Rejected
H3 Controller 내부 N+1 / DB slow query Controller 는 DB 접근 없이 upstream HTTP 하나만 호출 (issue_types_controller.rb:4-11), postgresql metric 무데이터 Rejected
H4 Kinesis 계열 sibling incident (a54e2505, us-west-2) 와 동일 원인 같은 svc:cupixworks-api::unknown 인시던트로 묶임 sibling 은 us-west-2 Kinesis TCP timeout (별도 fingerprint), 본 클러스터는 ap-southeast-2 issue-service latency — 리전과 downstream 상이 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/http_client.rb:12-24 (및 post/put/delete/patch 형제 메서드) 의 RestClient.get(url, headers) 호출에 timeout / open_timeout 명시. Rails 요청 스레드가 무제한 blocking 되지 않도록 하고, timeout 시 발생하는 RestClient::Exceptions::ReadTimeout (or Errno::ETIMEDOUT) 을 상위에서 rescue 해 5xx / graceful 응답으로 전환.
  • app/services/cupix/issue_service.rb:82-107list 메서드에서 timeout 발생 시 최소 캐시 응답 or 사용자에게 부분 실패를 명시적으로 알리는 경로 검토 (구현은 별도 티켓).

단기 개선 (1주 이내)#

  • ap-southeast-2 리전의 issue-service latency / 오류율에 대한 상시 대시보드와 알림 추가. Slack #alerts 로 리전 latency p95 > 5s 5분 지속 시 페이지.
  • Cupix::HttpClient 에 옵션 시그니처 확장: timeout: / open_timeout: / read_timeout: 파라미터 지원, 콜사이트별로 SLA 에 맞게 설정 가능하게.

장기 개선 (재발 방지)#

  • 모든 outbound HTTP 클라이언트에 대해 default timeout 표준(open 5s / read 10s 수준) 을 조직 차원에서 강제하는 lint / RuboCop rule 도입.
  • Cross-service latency 를 위한 circuit breaker (예: stoplight, semian) 도입 검토. Upstream 이 지속적으로 느릴 때 fast-fail 하여 caller 스레드 pool 을 보호.

Monitoring#

  • APM: Api::V1::IssueTypesController#index p95 duration
  • Region-level: cupixworks-api avg/p95 latency by region
  • Upstream: issue-service HTTP 응답 시간 (별도 서비스에서 metric 노출 필요 — 현재 service:cupix-issue-service 로그 미존재)
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::issuetypescontroller#index}
text
avg:trace.rack.request.duration{service:cupixworks-api,region:ap-southeast-2}
text
max:trace.rack.request.duration{service:cupixworks-api,env:production} by {region}

Risk Assessment#

  • Risk level: medium — 사용자 관점 UX 저하 (요청당 19s 지연) 이나 실제 에러(5xx) 는 없고 단발 발생. 다만 timeout 미설정으로 인해 upstream 이 심각히 느려지면 Puma 워커 pool 이 소진되어 서비스 전체 장애로 확산될 수 있음.
  • 예상 복잡도: standard — Cupix::HttpClient 에 timeout 옵션 추가는 트리비얼하지만, 콜사이트별 rescue 확장 및 regression 테스트가 필요.