failed to get element records - error: Timed out connecting to server
RCA: failed to get element records - error: Timed out connecting to server
Overview#
What Happened#
2026-07-08 21:58 KST, cupixworks-api production (us-west-2) 에서 GET /api/v1/siteinsights/element_records 요청 하나가 502 로 실패했다. 원인은 하위 Siteinsights microservice (AWS API Gateway) 로의 TCP 연결이 60초 동안 열리지 않아 RestClient::Exceptions::OpenTimeout 이 발생한 것. 14일 retention 안에서 동일 exception 은 총 3건 (2개 엔드포인트) 뿐이라 systemic 이슈가 아니라 transient network/upstream 이벤트로 판단된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::System (원인: RestClient::Exceptions::OpenTimeout) |
| exception.message | failed to get element records - error: Timed out connecting to server |
| top_frame | app/services/cupix/siteinsights_service.rb:118 |
| code | SYS20000 |
| deploy | production-us-west-2-20260708T0836Z0-5c918141-cupixworks |
| env | production, region us-west-2 |
| duration | 60027.9 ms (Net::HTTP default open_timeout = 60s) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| clark-vdc (team_id 87) | 1 | 단일 사용자(sebastian.boyle@clarkconstruction.com)의 facility_key=3fq7tj element_records 페이지네이션 요청 1건이 502 응답. UI 상 재시도 후 정상 응답 가능성이 높다. |
Timeline#
- 2026-07-08 17:36 KST — 최신 배포(
production-us-west-2-20260708T0836Z0-5c918141-cupixworks) 반영. - 2026-07-08 21:58:57 KST —
Api::V1::ElementRecordsController#index가 Siteinsights#{service_url}/element_records호출을 시작. TCP connect 실패로 60,027.9 ms 후RestClient::Exceptions::OpenTimeout발생. - 2026-07-08 21:58:57 KST —
Cupix::Errors::System(SYS20000) 로 rescue 되어 502 응답. 후속 재시도 로그 없음(즉시 해소된 것으로 보임).
Error Log#
failed to get element records - error: Timed out connecting to server
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-07-08 21:58 KST
- 최근 발생: 2026-07-08 21:58 KST
Root Cause Summary#
Cupix::SiteinsightsService.get_element_records! 가 Cupix::HttpClient.get 을 통해 프로덕션 Siteinsights API Gateway (https://6hdi0xzqqk.execute-api.us-west-2.amazonaws.com/api/element_records) 에 GET 요청을 보냈으나, TCP 연결이 60초 (Net::HTTP open_timeout 기본값) 동안 성립되지 않아 RestClient::Exceptions::OpenTimeout 이 던져졌다. 이는 downstream 서비스의 응답 지연이 아니라 connect 단계 실패이며, transient 네트워크/DNS/API Gateway edge 이슈로 추정된다. Cupix::HttpClient 의 재시도 로직은 HTTP status code 기반이라 (RETRIABLE_STATUS_CODES = [429, 502, 503, 504]), status code 가 없는 connect timeout 은 e.http_code == nil 이 되어 재시도가 트리거되지 않고 즉시 최상위로 전파된 것이 실패 폭을 키운 부수 요인이다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/element_records_controller.rb:4-9 - Failure point:
app/services/cupix/siteinsights_service.rb:118(RestClient GET) - Rescue / 재포장:
app/services/cupix/siteinsights_service.rb:121-124 - 502 변환:
app/controllers/api/v1/element_records_controller.rb:2, 37-39 - 재시도 필터:
lib/cupix/http_client.rb:12-24— status code 만으로 판정
Controller 는 Cupix::Errors::System 을 catch 하여 502 로 매핑한다.
class Api::V1::ElementRecordsController < Api::V1::ApiController
rescue_from Cupix::Errors::System, with: :redirect_to_502_error
def index
si_query_option = Cupix::QueryOption::Siteinsights.new(get_query_option, params)
element_records = Cupix::SiteinsightsService.get_element_records!(si_query_option, fields: @fields, current_user: @current_user, current_team: @current_team)
render_json 200, element_records
end
실제 HTTP 호출과 예외 재포장 지점.
def get_element_records!(query_option, fields: '', current_user: nil, current_team: nil)
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'facility_key is required') if query_option.facility_key.blank?
begin
params = {
facility_key: query_option.facility_key
}
params[:vendor_ids] = query_option.vendor_ids.join(',') if query_option.vendor_ids.present?
# ... 기타 params 병합 ...
params[:fields] = fields if fields.present?
response = Cupix::HttpClient.get("#{Cupix::Siteinsights.service_url}/element_records?#{params.to_param}")
JSON.parse(response.body)['result']
rescue RestClient::Exception => e
Cupix::Logger.error("failed to get element records - error: #{e.message}", class: self.name, function: __method__, facility_key: query_option.facility_key, params: params)
raise Cupix::Errors::System.new(code: 'SYS20000', reason: "failed to get element records - error: #{e.message}")
재시도 로직은 HTTP status code 기반이라 connect timeout 을 잡지 못한다. e.http_code 는 RestClient::Exceptions::OpenTimeout 에서 nil 이므로 RETRIABLE_STATUS_CODES.include?(nil) == false 로 즉시 raise 로 진행한다.
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
Production us-west-2 는 Cupix::Siteinsights.service_url 이 기본 case (region_code = usw2) 로 떨어져 https://6hdi0xzqqk.execute-api.us-west-2.amazonaws.com/api 를 호출한다.
when 'production'
case Cupix::Tesla.region_code
when 'euce1'
'https://4img1siwbf.execute-api.eu-central-1.amazonaws.com/api'
when 'apse2'
'https://5ufrbwf306.execute-api.ap-southeast-2.amazonaws.com/api'
when 'apne1'
'https://gi8rwxyr8j.execute-api.ap-northeast-1.amazonaws.com/api'
when 'apse1'
'https://30h7tj8fjg.execute-api.ap-southeast-1.amazonaws.com/api'
when 'cace1'
'https://px30557psl.execute-api.ca-central-1.amazonaws.com/api'
else
'https://6hdi0xzqqk.execute-api.us-west-2.amazonaws.com/api'
end
Log Evidence#
Datadog query 1 — 대표 에러 재현.
service:cupixworks-api "failed to get element records"
핵심 필드 (원문 JSON 발췌, 2026-07-08 21:58:57 KST).
{
"message": "failed to get element records - error: Timed out connecting to server",
"class": "Cupix::SiteinsightsService",
"function": "get_element_records!",
"level": "error",
"request_id": "23874b65-02e6-49e8-9002-b83eaf0f6f9f",
"facility_key": "3fq7tj",
"tenant": "cupix",
"environment": "production",
"region": "us-west-2",
"version": "production-us-west-2-20260708T0836Z0-5c918141-cupixworks",
"params": {
"per_page": 300,
"page": 6,
"level_ids": "72845",
"category_ids": "1029840,1029845,1029854,1029857,1029859",
"completed_until": "2026-07-06T18:03:04.000Z",
"facility_key": "3fq7tj"
}
}
연동된 request 로그 (같은 request_id).
{
"message": "[502] GET /api/v1/siteinsights/element_records (Api::V1::ElementRecordsController#index)",
"http": { "status_code": 502, "method": "GET", "url_details": { "path": "/api/v1/siteinsights/element_records" } },
"duration": 60027.9,
"db": 5.41,
"team": { "domain": "clark-vdc", "id": 87 },
"user": { "id": 50088, "email": "sebastian.boyle@clarkconstruction.com" },
"error": {
"reason": "failed to get element records - error: Timed out connecting to server",
"code": "SYS20000",
"class": "Cupix::Errors::System"
},
"request_id": "23874b65-02e6-49e8-9002-b83eaf0f6f9f"
}
duration: 60027.9 ms 는 Ruby Net::HTTP 의 open_timeout 기본값 60초와 정확히 일치 — 즉 응답 지연이 아니라 TCP connect 자체가 성립하지 못한 상태에서 timeout 된 것이다.
Datadog query 2 — 14일 window 확장 검색 (범위 넓힘).
service:cupixworks-api "Timed out connecting to server"
결과 3건, 모두 다른 시각/엔드포인트:
| Timestamp (KST) | Endpoint | Class |
|---|---|---|
| 2026-07-08 21:58 | GET /api/v1/siteinsights/element_records |
Cupix::SiteinsightsService |
| 2026-07-08 21:58 | [502] request 로그 (동일 요청) |
Cupix::Errors::System |
| 2026-07-07 20:13 | POST /api/v1/bims/forge_access_token |
RestClient::Exceptions::OpenTimeout |
Datadog query 3 — downstream(siteinsights) 서비스 자체 에러 로그 검증.
"siteinsights" status:error
2026-07-08T12:55Z ~ 13:05Z 구간에서 결과 0건. Downstream Lambda/API Gateway 측의 에러 로그가 관측되지 않는다는 것은, 요청이 애초에 도달하지 못했을 (TCP connect 실패) 가능성을 뒷받침한다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 하위 Siteinsights API Gateway/Lambda 로의 TCP connect 가 transient 하게 실패하여 60초 open_timeout | duration: 60027.9ms 가 Net::HTTP open_timeout 기본값과 일치, 예외 클래스가 RestClient::Exceptions::OpenTimeout (read timeout 아님), downstream service 에러 로그 0건 |
— | Confirmed |
| H2 | Downstream Siteinsights 서비스 자체 장애 (upstream outage) | 동일 시간대에 다른 팀/facility 의 실패가 있으면 outage 가능 | 14일 window 에서 element_records 관련 실패는 단 1건, downstream 로그도 error 없음. status-board 도 dep:* scope 아닌 svc:cupixworks-api::unknown 으로 분류. |
Rejected |
| H3 | Cupix::HttpClient 의 재시도 부족이 근본 원인 |
RETRIABLE_STATUS_CODES 가 status code 만 검사, OpenTimeout 은 e.http_code == nil 이라 재시도 스킵 |
connect timeout 자체가 근본 트리거이며, 재시도는 완화책. 이 자체가 root cause 는 아님. | Rejected (contributing factor) |
| H4 | 배포 (5c918141, 2026-07-08 08:36Z) 로 인한 회귀 |
배포 후 ~4시간 뒤 발생 | 배포 후 발생 건수 1건 뿐이며, 배포 전(2026-07-07) 에도 유사 OpenTimeout(forge_access_token) 발생. 배포와 무관한 패턴. |
Rejected |
| H5 | 페이지네이션(per_page=300, page=6) 이 원인 (파라미터 유발 부하) |
파라미터가 큰 편 | connect timeout 은 서버 처리 부하가 아니라 TCP handshake 실패 — 파라미터가 영향을 주는 계층 아님. | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 단일 transient 이벤트이고, 사용자 재시도로 회복되었다. 별도 hotfix 나 롤백 필요 없음.
단기 개선 (1주 이내)#
lib/cupix/http_client.rb재시도 로직을 확장하여 connect 계열 예외(RestClient::Exceptions::OpenTimeout,RestClient::ServerBrokeConnection) 도 재시도 대상에 포함.rescue절에서case e when RestClient::Exceptions::OpenTimeout, ... then retry형태로 처리하거나, status code 검사 앞단에 별도 조건 추가.- 재시도 회수 제한(
MAX_RETRIES = 3) 과 exponential backoff (2**(attempt-1) + rand(0..0.5)) 은 그대로 재활용. - 재시도 시 idempotent 하지 않은 verb(
post/put/patch/delete) 는 connect 실패인 경우에도 요청이 서버에 도달하지 않았음이 보장되므로 안전하다. 단, retry 대상 verb 정책은 리뷰어 협의 필요.
- 재시도 회수 제한(
Cupix::HttpClient호출부 전반에 대해 명시적open_timeout/read_timeout지정(RestClient::Request.execute(..., open_timeout: 5, timeout: 30)) 을 검토. 현재 60초 open_timeout 은 사용자 UX 관점에서 지나치게 길다.
장기 개선 (재발 방지)#
Cupix::SiteinsightsService및 다른 downstream HTTP client wrapper 들이 실질적으로 동일한 retry/timeout 패턴을 다루므로, 재시도·타임아웃 정책을Cupix::HttpClient한 곳에서 declarative 하게 정의하도록 통합. 예:HttpClient.get(url, retry_on: %i[open_timeout server_5xx], open_timeout: 5, read_timeout: 30).- Downstream 서비스별 SLO 대시보드에 outbound connect 실패율/응답 시간 histogram 추가. 현 배포 버전에는 outbound RestClient 호출 자체를 계측하는 명시적 메트릭이 확인되지 않음.
Monitoring#
Datadog release dashboard timeseries 위젯용 쿼리 (모두 sum: prefix, .as_count() 로 count 를 그리도록 구성).
Outbound connect timeout 전반 트렌드 (모든 downstream) — 초기값이 낮으므로 개선 후 지속적으로 0에 수렴하는지 확인.
sum:trace.rack.request.errors{service:cupixworks-api,error_type:RestClient::Exceptions::OpenTimeout,env:production}.as_count()
Siteinsights element_records 502 응답 카운트.
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api/v1/element_records_controller#index,http.status_code:502,env:production}.as_count()
로그 기반 대체 쿼리 (trace metric 미구성 시).
logs("service:cupixworks-api \"failed to get element records - error: Timed out connecting to server\"").index("*").rollup("count").by("region").last("1h") > 5
위 로그 쿼리는 monitor 문법이므로 dashboard timeseries 에 넣을 경우 아래처럼 count_by_status 계열 metric 으로 치환할 것.
sum:cupix.logs.errors{service:cupixworks-api,@class:Cupix::SiteinsightsService,@function:get_element_records!,env:production}.as_count()
알림 임계값 제안: 1분 rolling window 에서 동일 exception 이 3회 이상이면 경보 (systemic outage 신호로 간주).
Risk Assessment#
- Risk level: low. 14일 retention 내에서 단 1건, 사용자 재시도로 회복 가능한 502. 사용자 impact 는 한 사용자·한 페이지네이션 요청.
- 예상 복잡도: trivial. 코드 수정 자체는 필요 없음. 단기 개선(retry 확장)만 진행한다면
lib/cupix/http_client.rb5개 메서드에 동일 패턴 적용 (standard 수준).