Failed to put records: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (executi
RCA: Failed to put records — Kinesis us-west-2 TCP timeout
Overview#
What Happened#
2026-07-17 22:45 KST, cupixworks-api (production, us-west-2) 에서 model update callback 이 site-insights 이벤트를 Kinesis 스트림에 동기 publish 시도하다가 kinesis.us-west-2.amazonaws.com:443 로의 TCP 소켓 open 이 timeout 되며 실패했다. 단일 요청 1회 발생이며, 같은 초에 caller-level(Failed to create event) 과 SDK-level(Failed to put records) 두 개의 error 로그가 페어로 남았다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Seahorse::Client::NetworkingError (wraps Net::OpenTimeout) |
| exception.message | Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired) |
| top_frame | lib/cupix/aws/kinesis.rb:9 (kinesis_client.put_records) |
| runtime | Ruby / Rails (cupixworks-api) |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-api (site-insights event publish) | 2 로그 / 1 요청 | 단일 API request 의 모델 업데이트 콜백이 500 으로 실패했을 가능성 (site-insights 이벤트 소실) |
Timeline#
- 2026-07-17 22:45:51 KST —
Eventable::Events::Update.create_event콜백이Cupix::EventService.publish_event를 호출해 Kinesisput_records!로 진입. - 2026-07-17 22:45:51 KST —
Aws::Kinesis::Client#put_records내부의 HTTP open 이 timeout,Net::OpenTimeout (execution expired)발생.Cupix::Aws::Kinesis.put_records!의 rescue 블록이Failed to put records: ...를 로깅하고 raise. - 2026-07-17 22:45:51 KST —
Eventable::Events::Base.create_event의rescue StandardError가 이 예외를 잡아Failed to create event: ...를 로깅하고 재-raise. 이후 자동 재시도 없음. - 2026-07-17 22:45:51 KST — status-board 가 sibling cluster
28729bf3-51f6-47d0-80ba-c79bd2a8ee61와 함께svc:cupixworks-api::unknown인시던트2026-07-17-svc-cupixworks-api--unknown-2로 자동 그룹핑 (동일 초, 이후 재발 없음 — resolved).
Error Log#
Failed to put records: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-07-17 22:45 KST
- 최근 발생: 2026-07-17 22:45 KST
Root Cause Summary#
Production API 프로세스에서 Kinesis us-west-2 endpoint 로의 TCP connect 가 aws-sdk-core 의 기본 http_open_timeout (15초) 안에 완료되지 못했다. Ruby 의 Net::OpenTimeout 은 "execution expired" 메시지로 표면화되며, Cupix::Aws::Kinesis.put_records! 는 이를 잡아 로깅한 뒤 그대로 raise 한다. 상위 호출자인 Cupix::EventService.publish_event 는 RestClient::Exception 만 rescue 하므로 이 Seahorse::Client::NetworkingError 는 통과하고, model callback 을 통해 실행 중이던 API 요청 자체가 실패한다. 발생 빈도(단발, 14일 창에서 유일)로 볼 때 지속적 문제가 아니라 순간적 네트워크/TLS handshake 지연(전송량 스파이크, VPC endpoint 흔들림 등)에 노출된 것으로 판단되며, 애플리케이션 수준에서는 동기 publish 를 API request path 에 두는 설계가 이 순간적 이슈를 사용자 요청 실패로 확대시키는 구조적 원인이다.
Technical Analysis#
Code Path#
Entry point: model callback → Eventable::Events::Update.create_event(self) (예: app/models/concerns/notifiable/record.rb:27).
Eventable::Events::Base.create_event가 이벤트를 만든 뒤 동기로 Kinesis publish 를 호출한다.
def create_event(model)
return nil if invalid_event?(model)
begin
event = _create_event(model)
reason = extract_reason(event, model)
properties = build_properties(model)
track_event(model, reason, properties)
Cupix::EventService.publish_event([event])
Cupix::Event.publish(event.serializable_hash(stringify_nested_fields: false))
rescue StandardError => e
Cupix::Logger.error("Failed to create event: #{e.message}", function: __method__, class: self.name, model: model.class.to_s, id: model.id, event_params: model.event_params, error: e)
raise e
else
model.event_created!
event
end
end
Cupix::EventService.publish_event가 Kinesis SDK 를 직접 호출한다. 예외 rescue 는RestClient::Exception만 다루므로 Seahorse networking error 는 통과한다.
def self.publish_event(events = [])
return if events.blank?
return if %w[development test].include?(Rails.env)
return unless release_date_by(Rails.env)
_event = events.first
records = events.map do |event|
{
data: event.serializable_hash.to_json,
partition_key: Current.request_id || SecureRandom.uuid
}
end
# ... stream_name 결정 ...
response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })
# ...
rescue RestClient::Exception => e
Cupix::Logger.error("Failed to publish event: #{e.message}", ...)
raise Cupix::Errors::System.new(code: 'SYS20000', reason: "Failed to publish event: #{e.message}")
end
- Failure point —
Cupix::Aws::Kinesis.put_records!는 SDK 호출 직전/직후에 아무 timeout 옵션도 지정하지 않아 aws-sdk-core 의 기본값(http_open_timeout: 15s,retry_limit: 3) 을 사용한다. rescue 는 로그만 남기고 재-raise 한다.
def put_records!(opts = {})
raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'stream_name is blank') if opts[:stream_name].blank?
raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'records is blank') if opts[:records].blank?
kinesis_client.put_records({
stream_name: opts[:stream_name],
records: opts[:records]
})
rescue => e
Cupix::Logger.error("Failed to put records: #{e.message}", class: self.name, function: __method__, stream_name: opts[:stream_name], record_count: opts[:records]&.size, record_sizes: opts[:records]&.map { |r| r[:data]&.bytesize })
raise e
end
def kinesis_client
@kinesis_client ||= ::Aws::Kinesis::Client.new(region: ::Cupix::Tesla.region)
end
기대 동작 vs 실제 동작:
- 기대: Kinesis publish 실패는 사용자 요청과 분리되어 별도 워커(
KinesisPutRecordsWorker,retry: 3) 에서 흡수되고 재시도되어야 한다. Site-insights 자체 경로(KinesisPublisher) 는 이미 Sidekiq 로 분리되어 있다 (app/services/cupix/siteinsights_service/kinesis_publisher.rb:10). - 실제:
Cupix::EventService.publish_event는 위 워커 경로를 사용하지 않고 request thread 에서put_records!를 직접 호출한다. 순간적 네트워크 지연이 API 요청 실패로 이어지고, 애플리케이션 수준의 재시도가 없다.
Log Evidence#
Datadog query used:
service:cupixworks-api "kinesis.us-west-2.amazonaws.com"
Time range: 2026-07-17T13:00:00Z → 2026-07-17T14:15:00Z (2 hits).
두 로그가 같은 초에 페어로 발생:
{
"timestamp": "2026-07-17 22:45:51",
"status": "error",
"message": "Failed to put records: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)",
"class": "Cupix::Aws::Kinesis",
"function": "put_records!"
}
{
"timestamp": "2026-07-17 22:45:51",
"status": "error",
"message": "Failed to create event: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)",
"class": "Eventable::Events::Update",
"function": "create_event",
"error": {
"msg": "Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)"
}
}
확장 검색 결과:
service:cupixworks-api status:error "execution expired"(now-24h) → 위 2건만.service:cupixworks-api status:error(13:40Z–13:55Z) → 위 2건만 (window 내 다른 에러 없음).- Status board:
svc:cupixworks-api::unknownscope,active: null, 이 클러스터는 sibling28729bf3-...와 함께 인시던트2026-07-17-svc-cupixworks-api--unknown-2로 자동 그룹핑되었고 이미 resolved.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 순간적 네트워크/TLS 지연으로 Kinesis endpoint TCP open 이 aws-sdk-core 기본 http_open_timeout (15s) 을 초과 |
메시지가 Net::OpenTimeout 시그니처인 "execution expired". 14일 창에서 유일 1회, 동일 초 sibling 만 존재. status-board 로 이미 resolved. |
— | Confirmed |
| H2 | Kinesis 서비스의 광범위 outage (dep:kinesis 장애) |
— | status-board 가 dep:* 대신 svc:cupixworks-api::unknown 으로 그룹핑함. 같은 시간대 다른 서비스에서 kinesis 실패 없음. AWS us-west-2 kinesis 로 향하는 다른 클라이언트/워커의 실패 로그 없음 |
Rejected |
| H3 | 애플리케이션 버그 (잘못된 stream_name, 잘못된 records payload) | — | Cupix::Aws::Kinesis.put_records! 는 blank stream/records 를 명시적으로 Cupix::Errors::Argument 로 raise 하며, 실제 로그는 그 예외가 아닌 TCP 소켓 timeout. records 자체가 문제라면 SDK 는 ProvisionedThroughputExceededException 등 응답 레벨 에러를 반환했을 것이나 그런 흔적 없음. |
Rejected |
| H4 | Ruby 프로세스 fd 고갈/커넥션 풀 소진으로 새 소켓 open 불가 | 이론적으로 Net::OpenTimeout 유발 가능 |
같은 시간 창에서 다른 AWS/HTTP 클라이언트의 유사 timeout 이 관측되지 않음. 프로세스가 문제라면 후속 요청도 실패했어야 함. | Rejected (uncertain — process-level metric 확인 안 됨, needs verification via system.net.* metric 확인) |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 단발성 순간 네트워크 지연으로 확인되었고 자동 그룹 인시던트도 이미 resolved 상태이다. 코드 변경 없이도 재현/재발이 관측되지 않는다.
단기 개선 (1주 이내)#
lib/cupix/event_service.rb:43의 동기put_records!호출을 이미 존재하는 비동기 경로(KinesisPutRecordsWorker—app/workers/kinesis_put_records_worker.rb:6,retry: 3, queue:aws) 로 이관하는 방향을 검토.KinesisPublisher.publish(app/services/cupix/siteinsights_service/kinesis_publisher.rb:10) 가 이미 batch → worker 패턴을 구현하고 있으므로 재사용 가능. 이렇게 하면 순간적 Kinesis 네트워크 지연이 API request path 를 실패시키지 않고, Sidekiq 3회 재시도로 자연 흡수된다.Cupix::EventService.publish_event의rescue RestClient::Exception는 실제 발생하는 예외(Seahorse::Client::NetworkingError,Aws::Kinesis::Errors::ServiceError) 를 커버하지 못한다. 동기 호출을 유지한다면 최소한rescue Seahorse::Client::NetworkingError, Aws::Kinesis::Errors::ServiceError를 추가해 log level 을warn으로 낮추고 상위로 raise 하지 않는 방향을 검토 (event publish 실패가 API 성공/실패의 하드 게이트인지 도메인 확인 필요).Aws::Kinesis::Client.new에 명시적http_open_timeout/retry_limit을 설정해 request path 에서 최악 15s block 이 튀는 것을 3~5s 수준으로 낮추는 것을 검토.
장기 개선 (재발 방지)#
- Site-insights, model event 등 "부수 효과" 성격의 외부 pub/sub 은 원칙적으로 request path 에서 분리하고, 실패 시 재시도 가능한 Sidekiq 워커로 위임하는 컨벤션을 문서화.
KinesisPublisher를 표준 경로로 하고EventService.publish_event의 직접 SDK 호출을 제거. - Kinesis publish 실패에 대한 서비스 레벨 SLO/알림 정책 정의: 예) 5분 rolling 3회 이상일 때만 페이지, 단발은 warn 으로.
Monitoring#
writing-datadog-monitoring-queries skill 규칙에 따라 timeseries 위젯에 그대로 붙여 넣을 수 있는 형식으로 작성.
- Kinesis put_records 실패 건수 (per-minute count, cupixworks-api):
count:logs("service:cupixworks-api @class:Cupix::Aws::Kinesis @function:put_records! status:error").index("*").rollup("count").by("environment")
- Kinesis TCP timeout 특정 문자열 발생:
count:logs("service:cupixworks-api \"kinesis\" \"execution expired\" status:error").index("*").rollup("count")
- Event publish 실패로 인한 create_event 실패:
count:logs("service:cupixworks-api @function:create_event status:error \"Failed to create event\"").index("*").rollup("count").by("class")
알림 방향 (본 위젯 쿼리와 별개, Monitors 에서 threshold 설정): 5분 rolling 에서 첫 쿼리가 3 이상이면 warn, 10 이상이면 alert.
Risk Assessment#
- Risk level: low — 단발성, 이미 resolved, 사용자 체감 영향 최소(단일 요청, 이벤트 telemetry 손실 1건).
- 예상 복잡도: trivial (단기 개선까지 반영 시 standard — worker path 이관 및 rescue clause 조정 포함).