ES /docs

Net::OpenTimeout: Failed to open TCP connection to sqs.us-west-2.amazonaws.com:443 (execution expired)

RCA: Net::OpenTimeout — Failed to open TCP connection to sqs.us-west-2.amazonaws.com:443

Overview#

What Happened#

tesla (Rails API/Sidekiq worker, APM span net/http)가 AWS SQS 엔드포인트(sqs.us-west-2.amazonaws.com:443)로 메시지를 전송하려고 TCP connection을 열던 중 Net::OpenTimeout (execution expired)가 발생했다. 이는 애플리케이션 코드 결함이 아니라 AWS 네트워크 경로의 일시적 connect timeout이며, 16개월에 걸쳐 469건(<1/day) 발생한 저빈도 transient 이슈다. 동일 시간대 Datadog 로그를 보면 SQS뿐 아니라 Kinesis·S3 등 다른 AWS 엔드포인트에도 동일한 execution expired connect timeout이 광범위하게 나타나, SQS 특정 코드 경로가 아닌 인프라·네트워크 레이어의 transient 현상임이 확인된다.

Quick Facts#

Field Value
exception.class Net::OpenTimeout
exception.message Failed to open TCP connection to sqs.us-west-2.amazonaws.com:443 (execution expired)
top_frame net/http (Ruby stdlib, AWS SDK 하위)
runtime Ruby / aws-sdk-core (~> 3, >= 3.127.0)
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
tesla (cupixworks-api / -worker) 469 (16개월 누적) SQS enqueue 일시 실패. SDK 재시도 및 Sidekiq retry로 자가 복구. 사용자 영향 미미

Timeline#

  1. 2025-04-17 01:24 KST — 최초 발생 (first_seen, Representative Error 샘플)
  2. 2026-08-04 15:02 KST — 최근 발생 (last_seen)
  3. 2026-08-04 (동일 시간대) — Datadog 로그상 Kinesis·S3 등 다른 AWS 엔드포인트에도 동일 execution expired connect timeout 다발 (인프라 레이어 transient 확인)

Error Log#

Datadog Logs

text
Failed to open TCP connection to sqs.us-west-2.amazonaws.com:443 (execution expired)

Impact#

  • Service: net/http (APM 계측 span — 실제 앱은 tesla)
  • 발생 횟수: 469 (16개월 누적, <1/day)
  • 최초 발생: 2025-04-17 01:24 KST
  • 최근 발생: 2026-08-04 15:02 KST

Root Cause Summary#

net/http는 실제 서비스가 아니라 Datadog APM의 Net::HTTP 자동 계측 span 이름이다(config/initializers/datadog.rb에는 net/http에 대한 명시적 service_name override가 없어 기본값 net/http가 유지됨). AWS SDK for Ruby(aws-sdk-core v3)의 SQS client는 내부적으로 Ruby stdlib Net::HTTP로 HTTPS 요청을 보내므로, SQS로의 TCP connect가 지연되면 Net::OpenTimeout: ... (execution expired)가 이 span에서 관측된다. execution expired는 TCP 3-way handshake(connect) 단계가 SDK의 connect timeout 안에 완료되지 못했음을 의미하며, 원인은 AWS 측 endpoint 응답 지연·경로상 패킷 손실·SNAT/포트 고갈 등 일시적 네트워크 조건이다. tesla의 SQS 전송 코드에는 이를 유발하는 결함이 없다 — 모든 전송 경로가 예외를 rescue하거나 SDK·Sidekiq 재시도로 자가 복구한다. 따라서 코드 수정으로 제거할 수 있는 버그가 아니라 transient infra noise다.

Technical Analysis#

Code Path#

  • Entry point (전송 경로 1, 정상 rescue): app/models/job.rb:61 Job#send_message
  • Entry point (전송 경로 2, Sidekiq): app/workers/sqs_send_message_worker.rb:7 SqsSendMessageWorker#perform
  • Entry point (전송 경로 3, agent client): lib/cws/base_client.rb:46 Cws::BaseClient#invoke!
  • SQS client 생성: app/models/concerns/aws_adapter/sqs.rb:8-10
  • Failure point: net/http (Ruby stdlib) — AWS SDK가 SQS로 TCP connect를 여는 지점

APM span 이름의 근거 — datadog.rb에는 net/http에 대한 명시적 service_name이 없다:

config/initializers/datadog.rb:43-49ruby
    c.tracing.instrument :aws, service_name: global_service_name + '-aws'
    c.tracing.instrument :elasticsearch, service_name: global_service_name + '-elasticsearch'
    c.tracing.instrument :ethon, service_name: global_service_name + '-ethon'
    c.tracing.instrument :excon, service_name: global_service_name + '-excon'
    c.tracing.instrument :faraday, service_name: global_service_name + '-faraday'
    c.tracing.instrument :redis, service_name: global_service_name + '-redis'
    c.tracing.instrument :rest_client, service_name: global_service_name + '-rest_client'

SQS client는 옵션만 받아 그대로 Aws::SQS::Client를 생성한다 (별도 timeout override 없음 → SDK 기본 connect timeout 사용):

app/models/concerns/aws_adapter/sqs.rb:8-10ruby
      def sqs_client(options = {})
        Aws::SQS::Client.new(options)
      end

전송 경로 1(Job#send_message)은 예외를 rescue하고 error 로그 후 false를 반환한다 — unhandled crash가 아니다. 기대 동작(SQS 전송 성공)과 실제 동작(connect timeout)의 gap은 코드가 아니라 네트워크에서 발생:

app/models/job.rb:119-133ruby
    begin
      sqs_params = { queue_url: _queue_url, message_body: message_body.to_json }
      sqs_params.merge!(_fifo_params) if _fifo_params.present?
      sqs_client.send_message(sqs_params)
    rescue => e
      Cupix::Logger.error(
        "Failed to send SQS message - queue_url: #{_queue_url}, job_id: #{id}, jobable_type: #{jobable_type}, jobable_id: #{jobable_id}, error: #{e.class.name}: #{e.message}",
        class: self.class.name,
        function: __method__,
        backtrace: e.backtrace&.first(5)
      )
      false
    else
      true
    end

전송 경로 2(SqsSendMessageWorker)는 명시적으로 SDK 재시도(max_attempts: 3, retry_mode: :standard)를 설정하고 Sidekiq retry: 1까지 두어, transient timeout에 대해 다중 방어선을 갖는다. 예외는 rescue되어 error 로그로만 남는다:

app/workers/sqs_send_message_worker.rb:5-27ruby
  sidekiq_options queue: :aws, retry: 1

  def perform(queue_url, message, message_group_id = '', message_deduplication_id = '', options_json = '{}')
    options = JSON.parse(options_json, symbolize_names: true)

    begin
      if queue_url.end_with?('fifo')
        _sqs_client(options).send_message(queue_url: queue_url, message_body: message, message_group_id: message_group_id, message_deduplication_id: message_deduplication_id)
      else
        _sqs_client(options).send_message(queue_url: queue_url, message_body: message)
      end

      Cupix::Logger.debug('Sent message to SQS', class: self.class, function: __method__)
    rescue => e
      Cupix::Logger.error("Failed to send message to SQS: #{e.message}", class: self.class.name, function: __method__, error: e.message)
    end
  end

  private

  def _sqs_client(options)
    sqs_client({ max_attempts: _max_attemps(options), retry_mode: _retry_mode(options) })
  end

aws-sdk-core v3는 Net::OpenTimeoutSeahorse::Client::NetworkingError로 감싸 retryable transient error로 취급하고 기본 max_attempts 횟수만큼 재시도한다. 따라서 이 예외가 최종적으로 관측되었다는 것은, 재시도 창 내내 connect timeout이 지속된 실제 transient 네트워크 사건이 있었음을 의미한다.

Log Evidence#

SQS 특정 에러 로그는 최근 14일 내 존재하지 않는다 (예외가 rescue되거나 APM span에서만 포착되어 error 로그가 남지 않음, 또는 et: 이슈 특성상 occurrence가 14일 log retention 밖에 존재).

사용한 Datadog 쿼리 — SQS 전송 실패 로그 (0건):

text
service:cupixworks-worker "Failed to send SQS message"
service:cupixworks-api "Failed to send SQS message"
service:cupixworks-worker "Failed to send message to SQS"
service:cupixworks-worker "Failed to open TCP connection to sqs"

위 4개 쿼리 모두 now-14d 범위에서 0건. 즉 SQS 전송 경로에서 최근 unhandled/logged 실패는 없다.

반면 동일한 execution expired connect timeout 시그니처는 다른 AWS 엔드포인트에서 최근에도 다발한다. 쿼리:

text
service:cupixworks-worker "execution expired"

결과 (2026-08-04, 47건 이상):

json
{
  "timestamp": "2026-08-04 18:36:25",
  "status": "error",
  "message": "flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)",
  "class": "Record",
  "function": "flush_geo_coordinate"
}
json
{
  "timestamp": "2026-07-24 17:40:33",
  "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!"
}

SQS enqueue 자체는 정상 진행 중임을 보여주는 info 로그 (쿼리 service:cupixworks-api "sqs.us-west-2", 2026-08-04):

json
{
  "timestamp": "2026-08-04 18:35:52",
  "status": "info",
  "message": "Sending message to https://sqs.us-west-2.amazonaws.com/002596530511/cupix-tesla-ece: {:job=>{:id=>1250206}, ...}",
  "class": "CreateSitetrackJob",
  "function": "send_message"
}

이는 SQS 코드 경로가 정상 동작 중이며, execution expired는 SQS 특정이 아니라 여러 AWS 엔드포인트를 가로지르는 인프라 레이어 transient 현상임을 입증한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 AWS/네트워크 경로의 일시적 TCP connect timeout (인프라 transient) execution expired가 SQS·Kinesis(kinesis.us-west-2)·S3(s3.me-south-1) 등 여러 AWS 엔드포인트에서 동일하게 발생 (Datadog execution expired 쿼리 47+건); connect(handshake) 단계 timeout; 저빈도 469/16mo (<1/day) Confirmed
H2 SQS 전송 코드의 미처리 예외/버그 (unhandled crash) 3개 전송 경로 모두 rescue 존재 (job.rb:123, sqs_send_message_worker.rb:18, base_client.rb:92 #invoke); Job#send_message SQS 실패 로그 0건 ("Failed to send SQS message" now-14d) Rejected
H3 SDK 재시도 부재로 transient timeout이 그대로 노출되는 resilience gap aws_adapter/sqs.rb는 timeout override 없이 SDK 기본값 사용 SqsSendMessageWorkermax_attempts:3 retry_mode::standard + Sidekiq retry:1 설정; aws-sdk-core v3가 Net::OpenTimeout을 retryable로 취급해 기본 재시도 — 예외 노출은 재시도 소진 후의 실제 사건 Rejected
H4 특정 SQS 큐/리전 설정 오류 sqs.us-west-2 (기본 리전) 대상 Representative가 us-west-2(정상 primary 리전)이고 최근 info 로그상 동일 리전으로 정상 enqueue 중; 설정 오류라면 항상 실패해야 하나 저빈도 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 코드 변경 불필요. transient AWS connect timeout으로, 애플리케이션 코드 결함이 아니며 SDK 재시도·Sidekiq retry·rescue로 자가 복구된다.

단기 개선 (1주 이내)#

  • (선택) Error Tracking 노이즈 감소: net/http span에서 발생하는 Net::OpenTimeout(SQS/Kinesis/S3 connect timeout)은 이미 앱 레벨에서 rescue되어 error 로그로 남으므로, Error Tracking에서 이 et: 이슈를 mute/ignore 처리해 알람 노이즈를 줄이는 것을 고려. 저빈도(<1/day)이므로 우선순위는 낮다.

장기 개선 (재발 방지)#

  • (선택) aws_adapter/sqs.rbsqs_client가 명시적 retry_mode/max_attempts를 갖지 않는 경로(Job#send_message, Cws::BaseClient)에 대해 SqsSendMessageWorker와 동일한 재시도 정책을 표준화해 transient timeout 흡수력을 통일. 이는 버그 수정이 아니라 resilience 일관성 개선이다.
  • 반복적으로 특정 리전에서 connect timeout이 급증한다면 VPC endpoint(SQS interface endpoint) 도입 또는 SNAT/NAT gateway 포트 고갈 여부를 인프라 측에서 점검.

Monitoring#

net/http span의 AWS connect timeout 추이 (release dashboard timeseries widget용):

text
sum:trace.net_http.request.errors{service:net/http,env:production}.as_count()

worker의 execution expired connect timeout 로그 발생량:

text
sum:log.events{service:cupixworks-worker,status:error,@error.msg:*execution\ expired*}.as_count()

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없음 / noise)

Noise Verdict#

noise — SQS·Kinesis·S3 여러 AWS 엔드포인트에 걸친 일시적 TCP connect timeout(execution expired)으로, 모든 전송 경로가 rescue·SDK 재시도·Sidekiq retry로 자가 복구되는 transient 인프라 현상이며 수정할 코드 결함이 없다.