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#
- 2026-07-18 03:21 KST — 관련 인시던트
2026-07-17-svc-cupixworks-api--unknown-3시작 (sibling clustera54e2505— Kinesis TCP timeout us-west-2, 별도 root cause). - 2026-07-18 03:45 KST — Trace
607179151302061438시작,IssueTypesController#index호출. - 2026-07-18 03:46:15 KST —
Cupix::IssueService#list에서fetching list from issue service successful.info 로그 (retry 없이 단일 요청 성공). - 2026-07-18 03:46:17 KST — Rails 최종 응답:
[200] GET /api/v1/issue_types(총 19142ms 소요). - 2026-07-18 03:45~03:55 KST — ap-southeast-2 리전 API avg latency 12s ~ 17s 지속.
Error Log#
{
"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-107→Cupix::HttpClient.get - Failure point (증폭 지점):
lib/cupix/http_client.rb:12-24— timeout 미설정으로 upstream slowdown 을 그대로 전파
Controller 는 매우 얇다. Upstream 응답 시간이 곧 이 endpoint 의 latency 가 된다.
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 된다.
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 이 느리면 요청 스레드는 무제한 대기한다.
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 동안 열려 있었다.
service:cupixworks-api trace_id:607179151302061438
[
{
"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-api 의 status:error 로그는 0건, Cupix::IssueService 에러 로그도 0건이다.
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 을 뒷받침한다.
avg:trace.rack.request.duration{service:cupixworks-api,region:ap-southeast-2} window=3h
정상 구간: 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(orErrno::ETIMEDOUT) 을 상위에서 rescue 해 5xx / graceful 응답으로 전환.app/services/cupix/issue_service.rb:82-107의list메서드에서 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#indexp95 duration - Region-level:
cupixworks-apiavg/p95 latency by region - Upstream: issue-service HTTP 응답 시간 (별도 서비스에서 metric 노출 필요 — 현재
service:cupix-issue-service로그 미존재)
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::issuetypescontroller#index}
avg:trace.rack.request.duration{service:cupixworks-api,region:ap-southeast-2}
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 테스트가 필요.