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#
- 2025-04-17 01:24 KST — 최초 발생 (
first_seen, Representative Error 샘플) - 2026-08-04 15:02 KST — 최근 발생 (
last_seen) - 2026-08-04 (동일 시간대) — Datadog 로그상 Kinesis·S3 등 다른 AWS 엔드포인트에도 동일
execution expiredconnect timeout 다발 (인프라 레이어 transient 확인)
Error Log#
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:61Job#send_message - Entry point (전송 경로 2, Sidekiq):
app/workers/sqs_send_message_worker.rb:7SqsSendMessageWorker#perform - Entry point (전송 경로 3, agent client):
lib/cws/base_client.rb:46Cws::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이 없다:
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 사용):
def sqs_client(options = {})
Aws::SQS::Client.new(options)
end
전송 경로 1(Job#send_message)은 예외를 rescue하고 error 로그 후 false를 반환한다 — unhandled crash가 아니다. 기대 동작(SQS 전송 성공)과 실제 동작(connect timeout)의 gap은 코드가 아니라 네트워크에서 발생:
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 로그로만 남는다:
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::OpenTimeout을 Seahorse::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건):
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 엔드포인트에서 최근에도 다발한다. 쿼리:
service:cupixworks-worker "execution expired"
결과 (2026-08-04, 47건 이상):
{
"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"
}
{
"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):
{
"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 기본값 사용 |
SqsSendMessageWorker는 max_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/httpspan에서 발생하는Net::OpenTimeout(SQS/Kinesis/S3 connect timeout)은 이미 앱 레벨에서 rescue되어error로그로 남으므로, Error Tracking에서 이et:이슈를 mute/ignore 처리해 알람 노이즈를 줄이는 것을 고려. 저빈도(<1/day)이므로 우선순위는 낮다.
장기 개선 (재발 방지)#
- (선택)
aws_adapter/sqs.rb의sqs_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용):
sum:trace.net_http.request.errors{service:net/http,env:production}.as_count()
worker의 execution expired connect timeout 로그 발생량:
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 인프라 현상이며 수정할 코드 결함이 없다.