EC2 Networking / Technical Advisory

Amazon EC2 Nitro V6 인스턴스의 TCP 연결 추적 유휴 타임아웃 변경 이해하고 대응하기

Nitro V6 인스턴스에서 보안 그룹 연결 추적의 TCP established 유휴 타임아웃 기본값이 짧아졌습니다. 무엇이 바뀌었는지, 어떤 워크로드가 왜 영향을 받는지, TCP keepalive를 중심으로 어떻게 대비하면 되는지 정리했습니다.

01. 작성 기준일 - 2026-08-29

02. 적용 대상 - Nitro V6 인스턴스(m8i, r8i 등)

03. 변경된 파라미터 - ConnectionTrackingSpecification의 TcpEstablishedTimeout

04. 주 출처 - AWS Networking & Content Delivery 블로그(2026-05-22)와 EC2 공식 문서

요약

주말 내내 트래픽이 없던 서비스에서 월요일 아침 첫 요청들만 유독 타임아웃으로 실패합니다. Karpenter가 노드를 교체한 뒤부터는 원인을 알 수 없는 연결 오류가 늘었는데, 부하 테스트를 아무리 돌려도 재현되지 않습니다. 최근 이런 증상을 겪었다면 애플리케이션 코드보다 먼저 확인할 것이 있습니다. 워크로드가 올라간 인스턴스 세대입니다.

2025년 6월부터 출시되고 있는 Nitro V6 기반 인스턴스(m8i, r8i 등)에서는 보안 그룹(security group) 연결 추적(connection tracking)의 TCP established 유휴 타임아웃(idle timeout) 기본값이 432,000초(5일)에서 350초로 짧아졌습니다. AWS Networking & Content Delivery 블로그는 2026년 5월 Best Practices for TCP Connection Management on EC2에서 이 변경의 배경과 권장 대응을 정리해 발표했습니다. 이 글에서는 해당 블로그와 EC2 공식 문서를 바탕으로 무엇이 바뀌었는지, 어떤 워크로드가 왜 영향을 받는지, 그리고 TCP keepalive를 중심으로 어떻게 대비하면 되는지 살펴봅니다.

01무엇이 바뀌었는가

변경된 것은 ENI(elastic network interface) 단위 설정인 TCP established 유휴 타임아웃의 기본값 하나입니다. API에서는 네트워크 인터페이스의 ConnectionTrackingSpecification에 포함된 TcpEstablishedTimeout 파라미터로 지정합니다. 60초에서 432,000초 사이로 조정하는 설정 범위는 예전 그대로이고 Nitro V6에서 기본값만 350초로 낮아졌습니다.

표 1. TCP established 유휴 타임아웃 사양
항목값
설정 범위60초~432,000초(5일)
기본값 - Nitro V6350초 (P6e-GB200 제외)
기본값 - 그 외 전 세대432,000초(5일), P6e-GB200 포함
설정 단위ENI
설정 경로AWS CLI, 시작 템플릿, 콘솔, AWS CloudFormation, Terraform
AWS 권고432,000초보다 작게 설정

P6e-GB200에는 350초 기본값이 적용되지 않고 다른 세대와 같은 432,000초가 유지됩니다. 다만 이 타입의 Nitro 버전 분류는 문서마다 다릅니다. Nitro 인스턴스 문서는 Nitro v5로 분류하고 연결 추적 문서는 Nitro V6의 350초 예외 타입으로 명시합니다. 표 1의 "제외" 표기는 연결 추적 문서를 따랐습니다. 분류가 어느 쪽이든 타임아웃 결론은 같습니다.

적용 대상을 판단할 때는 세대 번호와 Nitro 버전이 항상 일치하지는 않는다는 점에 주의가 필요합니다. Graviton4 기반 8세대인 M8g, C8g, R8g는 Nitro v5라서 이번 변경의 영향을 받지 않고, 9세대인 M9g, C9g는 Nitro v6라서 해당됩니다. 사용 중인 타입이 해당하는지는 Nitro 인스턴스 문서의 목록으로 확인합니다. 가상화 인스턴스 기준 목록입니다.

표 2. Nitro v6 가상화 인스턴스 타입 (Nitro 인스턴스 문서 기준)
분류Nitro v6 인스턴스 타입
범용M8a, M8azn, M8gb, M8gn, M8i, M8id, M8i-flex, M8in, M8idn, M8ine, M8ib, M8idb, M9g, M9gd
컴퓨팅 최적화C8a, C8gb, C8gn, C8i, C8id, C8i-flex, C8in, C8ine, C8ib, C9g, C9gd
메모리 최적화R8a, R8gb, R8gn, R8i, R8id, R8i-flex, R8in, R8idn, R8ib, R8idb, X8aedz, X8i
스토리지 최적화I8ge
가속 컴퓨팅G7, G7e, P6-B200, P6-B300
HPCHpc8a

베어메탈은 이 목록에서 -flex 계열과 M8ine, C8ine, 가속 컴퓨팅, HPC를 제외한 구성입니다. 혼동하기 쉬운 Nitro v5 타입은 M8g, M8gd, C7gn, C8g, C8gd, R8g, R8gd, X8g, I7ie, I8g, P5en, P6e-GB200, Trn2, Trn2u, Hpc7g입니다.

UDP 쪽은 원래부터 짧았습니다. 단방향이거나 요청-응답 한 번으로 끝나는 흐름에 적용되는 UdpTimeout 기본값이 30초(최대 60초), 스트림으로 분류된 흐름에 적용되는 UdpStreamTimeout 기본값이 180초(최소 60초)라서 이번 변경의 대상이 아닙니다.

용어 주의 - 커널 conntrack이 아닙니다

여기서 말하는 연결 추적은 Nitro 계층에서 보안 그룹, VPC Flow Logs, 네트워크 미터링을 뒷받침하는 기능으로, 리눅스 커널의 conntrack과는 다른 층위입니다. 공교롭게도 커널 파라미터 nf_conntrack_tcp_timeout_established의 기본값 역시 432,000초라 혼동하기 쉬운데, 이번 변경은 커널과 무관한 ENI, 하이퍼바이저 계층의 이야기입니다.

인스턴스 안에서 sysctl 값을 아무리 바꿔도 이 타임아웃은 달라지지 않습니다.

02변경 배경

2.1 연결 추적 할당량 고갈 방지

연결 추적 할당량(allowance)은 인스턴스가 쓸 수 있는 Nitro 리소스에 따라 유한합니다. 기본값이 5일이던 시절에는 FIN이나 RST를 받지 못한 유휴 연결과 고아(orphaned) 연결이 추적 테이블에 최대 5일씩 남았습니다. 이것이 쌓여 할당량을 소진하면 기존 엔트리가 닫히거나 타임아웃될 때까지 인스턴스가 신규 연결을 아예 수립하지 못합니다. connection timeout과 connection refused 오류, 백엔드 연결 수립 실패로 인한 로드 밸런서의 504 응답, Network Load Balancer(NLB)의 TCP_ELB_Reset_Count 지표 증가가 이때 나타나는 대표 증상입니다.

UDP를 비롯한 non-TCP 흐름은 타임아웃이 30~180초로 짧아 대규모로 누적될 가능성이 낮습니다. 누적 리스크가 사실상 TCP established 상태에 몰려 있었고, 이번 변경은 정확히 그 이슈를 완화하기 위함입니다.

2.2 다른 네트워킹 서비스와의 정렬

NLB, NAT 게이트웨이, Gateway Load Balancer는 이미 350초 유휴 타임아웃을 사용합니다. EC2 ENI에서만 5일이면 한쪽은 연결이 살아 있다고 믿고 다른 쪽은 이미 버린 half-open 상태가 생깁니다. 350초 기본값은 이런 계층 간 불일치를 줄이려는 선택입니다.

참고 - 설정 가능 옵션은 2023년 11월부터 존재

ENI 단위로 타임아웃을 조정하는 기능 자체는 새로운 기능이 아닙니다. 2023년 11월에 구성 가능한 연결 추적 유휴 타임아웃이 도입되어 모든 Nitro 세대에서 60초부터 432,000초까지 설정할 수 있었습니다. 이번에 변경된 부분은 기능 그 자체보다는 V6의 기본값입니다.

03영향 분석

3.1 silent drop이 만드는 까다로운 장애 양상

타임아웃으로 연결 추적 엔트리가 삭제될 때 양쪽 어디에도 FIN이나 RST가 전달되지 않습니다. 클라이언트 소켓은 ESTABLISHED 상태로 남아 있고, 다음 요청을 보내는 순간 패킷이 조용히 버려집니다. AWS 블로그에서는 이 현상을 silent drop이라고 부릅니다.

silent drop이 까다로운 데는 이유가 있습니다. 애플리케이션이 즉시 오류를 받지 못하고 자체 타임아웃에 걸릴 때까지 멈춰 있으므로 요청 지연이 수십 초 단위로 치솟습니다. 연결 풀은 죽은 연결을 정상으로 판단해 계속 재사용하므로 유휴 시간이 길었던 새벽이나 주말 직후의 첫 요청들이 집중적으로 실패하는 패턴이 나타납니다. 재현도 어렵습니다. 부하 테스트는 트래픽을 계속 흘리기 때문에 유휴 상태 자체가 만들어지지 않아 그대로 통과해 버립니다.

3.2 영향받는 워크로드

350초보다 긴 유휴 구간이 생기는 연결이라면 모두 점검 대상입니다.

  • 데이터베이스 연결 풀 - maxIdleTime이 350초보다 크거나 설정돼 있지 않은 경우로, 가장 흔한 피해 사례입니다.
  • IoT 텔레메트리 - 간헐적으로 전송하는 MQTT와 장기 세션이 해당합니다.
  • 마이크로서비스 간 persistent HTTP, gRPC 연결 - keep-alive로 재사용하는 풀이 대상입니다.
  • 웹소켓과 롱폴링 - 애플리케이션 하트비트 주기가 350초보다 길면 영향을 받습니다.
  • 배치 간격이 긴 워커, 유휴 상태로 열어 둔 SSH와 점프박스 세션

3.3 의도하지 않은 세대 이관

이번 변경에서 가장 주의할 지점은 이관이 결정이 아니라 자동으로 일어난다는 사실입니다. AWS도 블로그에서 이 부분을 명시적으로 경고합니다.

주의 - AWS가 블로그에서 명시적으로 경고하는 부분

Karpenter나 속성 기반 인스턴스 유형 선택(attribute-based instance type selection)을 사용하는 EC2 Auto Scaling 그룹은 조건에 맞는 최신 세대를 알아서 고릅니다. 아무 변경도 하지 않았는데 워크로드가 Nitro V6에 올라가고, 첫 징후는 노드 리프레시나 스케일링 이벤트 뒤에 나타나는 원인 불명의 연결 실패입니다.

Amazon EKS 환경이라면 두 가지를 더 확인해야 합니다. Karpenter NodePool에 인스턴스 제약이 없으면 m8i 같은 Nitro V6 타입이 선택될 수 있습니다. 이때 karpenter.k8s.aws/instance-generation 같은 세대 제약만으로는 Nitro 버전을 통제할 수 없습니다. 9세대인 M9g는 Nitro v6라서 걸러지지 않고 Nitro v5인 M8g만 불필요하게 배제됩니다. 통제가 필요하면 인스턴스 패밀리 목록으로 지정하고 근본 대응은 세대와 무관하게 동작하는 keepalive에 둡니다. 그리고 Amazon VPC CNI 기본 모드에서 파드는 노드의 ENI를 공유하므로 노드 ENI의 타임아웃이 파드에 그대로 적용됩니다.

3.4 계층 간 타임아웃 불일치

NLB 유휴 타임아웃을 1,800초로 늘려 두었는데 ENI가 350초라면 로드 밸런서는 연결을 유지하는 동안 EC2가 먼저 추적을 버립니다. 클라이언트 입장에서는 잘 되던 연결에 갑자기 문제가 발생한다는 것입니다. ENI 타임아웃만 조정하는 대응은 이런 함정을 만듭니다. 스택 전체를 정렬하는 방법은 대응 방안에서 이어서 다룹니다.

04대응 방안

우선순위 순으로 살펴봅니다. 1순위인 keepalive를 적용하면 나머지는 보완 수단이 되고, 적용하지 않으면 나머지 수단만으로는 세대가 혼재된 환경에서 안전을 보장하기 어렵습니다.

4.1 TCP keepalive 적용

keepalive는 유휴 연결에 주기적으로 프로브 패킷을 보내 연결이 어느 인프라 계층에서도 유휴 상태에 도달하지 않게 합니다. 타임아웃 값 자체를 무의미하게 만들기 때문에 EC2, 로드 밸런서, NAT 게이트웨이를 포함한 모든 계층에서 유효하고 Nitro 세대가 섞여 있어도 동작이 달라지지 않습니다.

ENI 타임아웃이 350초라면 프로브를 240초 이전에 시작하도록 설정해 여유를 확보합니다. 240초는 350초까지 110초를 남기는 값입니다. 상대 호스트가 이미 응답하지 않아 프로브가 전부 실패해도 240 + 60 × 3 = 420초면 소켓이 정리됩니다. 리눅스 커널의 keepalive 기본 시작 시간은 7,200초(2시간)로, 클라우드 인프라의 타임아웃을 막기에는 너무 깁니다. 값 조정이 필수입니다.

/etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 240    # 4분 유휴 후 프로브 시작
net.ipv4.tcp_keepalive_intvl = 60    # 60초 간격으로 프로브
net.ipv4.tcp_keepalive_probes = 3    # 3회 실패 시 연결 종료
주의 - 가장 흔히 놓치는 함정, SO_KEEPALIVE

위 sysctl은 프로브 간격을 정할 뿐이고 소켓별로 SO_KEEPALIVE가 켜져 있지 않으면 프로브가 아예 나가지 않습니다. 대부분의 OS에서 이 옵션의 소켓 기본값은 꺼짐입니다. sysctl만 넣고 keepalive 적용을 끝냈다고 판단하면 증상이 그대로 재발하므로 애플리케이션이나 드라이버 레벨에서 함께 켜야 합니다.

소켓을 직접 다루는 코드에서는 파이썬 기준으로 옵션을 이렇게 지정합니다.

애플리케이션 코드 - Python 소켓 옵션
import socket

# TCP_KEEPIDLE 계열 상수는 Linux 전용 (macOS는 TCP_KEEPALIVE, Windows에는 없음)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 240)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)

TCP_KEEPIDLE 계열 옵션까지 함께 넣으면 sysctl 기본값을 소켓 단위로 덮어쓸 수 있습니다. 주요 애플리케이션은 저마다 keepalive 설정 항목을 따로 둡니다.

표 3. 주요 애플리케이션의 keepalive 설정 항목
대상파라미터
PostgreSQL tcp_keepalives_idle, tcp_keepalives_interval, tcp_keepalives_count
Redis tcp-keepalive
HTTP 클라이언트, 메시징 라이브러리 대부분 자체 keepalive 옵션 제공, 드라이버별 확인 필요
Windows HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters의 KeepAliveTime, KeepAliveInterval

Redis의 tcp-keepalive 기본값은 300초라 350초 안에 들어오지만 여유가 50초뿐입니다. 명시적으로 더 낮춰 두는 편이 안전합니다.

Amazon EKS에서는 파드의 securityContext.sysctls로 net.ipv4.tcp_keepalive_* 값을 지정합니다.

Kubernetes Pod 스펙 - securityContext.sysctls
spec:
  securityContext:
    sysctls:
      - name: net.ipv4.tcp_keepalive_time
        value: "240"
      - name: net.ipv4.tcp_keepalive_intvl
        value: "60"
      - name: net.ipv4.tcp_keepalive_probes
        value: "3"

이 sysctl들은 Kubernetes 1.29부터 safe sysctl로 승격되었기 때문에 1.29 미만 클러스터에서는 unsafe sysctl 허용 설정이 필요합니다. 버전과 무관하게 hostNetwork: true 파드에서는 net.* 계열 sysctl 지정이 허용되지 않아 파드가 기동에 실패합니다. 두 경우 모두 노드 userData나 DaemonSet으로 노드 전체에 적용하는 방법이 해법입니다.

한 가지 한계도 알아 두면 좋습니다. TCP keepalive는 인프라의 타임아웃을 막아 주지만 원격 애플리케이션이 응답 불능에 빠진 상황은 TCP 연결이 온전해도 감지하지 못합니다. 그래서 많은 워크로드가 keepalive와 애플리케이션 레벨 하트비트를 함께 운영합니다.

4.2 ENI 타임아웃 명시적 설정

keepalive를 적용할 수 없는 연결에 350초보다 긴 유휴가 필요하다면 기본값에 기대지 말고 값을 명시합니다. AWS CLI에서는 ModifyNetworkInterfaceAttribute 작업으로 설정합니다.

AWS CLI - ModifyNetworkInterfaceAttribute
aws ec2 modify-network-interface-attribute \
  --network-interface-id eni-xxxxxxxx \
  --connection-tracking-specification TcpEstablishedTimeout=3600

시작 템플릿(launch template), 콘솔, AWS CloudFormation과 Terraform으로도 같은 값을 지정합니다. 신규 인스턴스에 적용하려면 시작 템플릿에 넣어야 하고, Karpenter와 Auto Scaling 그룹 경로도 마찬가지입니다. 다만 Karpenter의 EC2NodeClass에서 이 설정이 직접 노출되지 않을 수 있으니 사용 중인 버전의 지원 여부를 먼저 확인하고 지원되지 않으면 커스텀 시작 템플릿이나 keepalive 경로로 가는 편이 확실합니다.

4.3 연결 수명 주기 정리

연결 풀 설정을 함께 손보면 타임아웃과의 충돌 자체가 줄어듭니다. maxIdleTime을 350초보다 짧게 잡고, maxLifetime으로 연결을 주기적으로 교체하고 사용 전에 validation query나 health check로 살아 있는지 검증합니다. 풀 크기도 실제 동시성 요구에 맞게 줄입니다. 예를 들어 HikariCP에서는 두 값을 이렇게 잡습니다.

HikariCP 설정 - application.properties
# HikariCP: 유휴 300초에 정리(350초보다 짧게), 30분마다 연결 교체
idleTimeout=300000
maxLifetime=1800000

이름은 풀마다 달라서 MongoDB 드라이버는 maxIdleTimeMS, R2DBC 풀은 maxIdleTime처럼 대응 항목을 찾아 적용합니다.

원칙: 연결은 keepalive가 있는 활성 상태이거나 닫힌 상태, 둘 중 하나여야 하고 무기한 유휴 상태로 두지 않습니다. 장시간 유휴가 예상되면 닫았다가 필요할 때 다시 엽니다.

4.4 스택 전체의 타임아웃 정렬

AWS가 권고하는 구조는 애플리케이션이 가장 먼저 닫는 배치입니다. 예를 들어 NLB 1,800초, ENI 350초, 애플리케이션 60초처럼 안쪽 계층일수록 타임아웃이 짧으면 유휴 연결을 애플리케이션이 가장 먼저 닫고, 종료 신호(FIN)가 ENI와 로드 밸런서를 거쳐 외부 계층으로 전파됩니다. 어느 인프라 계층도 연결을 조용히 버리지 않게 됩니다. 순서가 반대면 silent drop이 발생합니다. Application Load Balancer(ALB) 뒤라면 기본 유휴 타임아웃이 60초라서 보통 ENI보다 먼저 연결이 정리됩니다. 다만 값을 늘려 잡았다면(최대 4,000초) ENI 350초와의 순서가 뒤집히지 않았는지 확인합니다.

TIMEOUT ALIGNMENT NETWORK LOAD BALANCER 유휴 타임아웃 1,800초 EC2 ENI 연결 추적 유휴 타임아웃 350초 애플리케이션 유휴 타임아웃 60초 유휴 연결을 가장 먼저 닫습니다 close() → FIN 안쪽이 먼저 닫으면 종료 신호가 바깥 계층으로 전파됩니다. 순서가 반대면 silent drop이 발생합니다. FIN 전파 애플리케이션 60초 < EC2 ENI 350초 < NLB 1,800초
그림 1. 안쪽 계층일수록 짧은 타임아웃 배치. 애플리케이션 60초, ENI 350초, NLB 1,800초 순서라야 종료 신호가 외부로 전파됩니다. (권고 구조 출처: Best Practices for TCP Connection Management on EC2, 2026-05-22)

그래서 아키텍처에 존재하는 모든 타임아웃 값을 문서화해 두는 것이 좋습니다. 애플리케이션, 로드 밸런서, NAT 게이트웨이, EC2 ENI가 대상입니다. 가장 신뢰할 수 있는 방법은 체인에서 가장 짧은 타임아웃보다 더 짧은 keepalive를 구현해 정렬 문제 자체를 피하는 것입니다.

반대로 타임아웃을 오히려 낮춰야 하는 경우도 있습니다. 공개 API나 데이터베이스 서버처럼 클라이언트 동작을 통제할 수 없는 서비스라면 ENI 타임아웃을 350초 기본값 또는 그 이하로 두는 편이 맞습니다. 유휴 클라이언트 연결이 누적되어 할당량을 소진하는 쪽이 더 위험하기 때문입니다.

05탐지 및 모니터링

ENA 드라이버가 노출하는 연결 추적 지표로 상태를 직접 확인할 수 있습니다. 인터페이스 이름이 AMI에 따라 eth0(Amazon Linux 2)이나 ens5(AL2023 등 예측 가능한 이름 체계)로 다르므로 이름부터 확인하고 실행합니다.

인스턴스 내부에서 실행
ip -br link                       # 인터페이스 이름 확인
ethtool -S eth0 | grep conntrack  # 이름이 ens5라면 바꿔서 실행

두 지표의 해석은 다음 표와 같고, 값의 급격한 변화가 곧 신호입니다.

표 4. 연결 추적 관련 ENA 지표
지표의미해석
conntrack_allowance_available 남은 추적 용량 갑자기 크게 뛰면 다수의 연결이 동시에 정리된 것으로, 타임아웃 이벤트의 신호입니다.
conntrack_allowance_exceeded 할당량 초과로 드롭된 패킷 수 스파이크는 신규 연결 실패가 진행 중이라는 뜻입니다.

이 중 conntrack_allowance_available은 2023년에 추가된 네트워크 성능 지표라서 리눅스 ENA 드라이버 2.8.1 이상이 필요합니다. 오래된 AMI라면 ethtool -i eth0으로 드라이버 버전부터 확인하세요.

프로덕션에서는 CloudWatch 통합 에이전트(1.246396.0 이상) 설정에 ethtool 섹션을 추가해 두 지표를 게시합니다.

CloudWatch 에이전트 구성 - amazon-cloudwatch-agent.json
{
  "metrics": {
    "metrics_collected": {
      "ethtool": {
        "interface_include": ["eth0"],
        "metrics_include": [
          "conntrack_allowance_available",
          "conntrack_allowance_exceeded"
        ]
      }
    }
  }
}

게시된 지표에는 ethtool_ 접두사가 붙습니다. 그래프와 경보를 만들 때는 ethtool_conntrack_allowance_available이라는 이름으로 찾습니다. 인터페이스 이름이 ens5라면 interface_include 값도 그에 맞춥니다. 여기에 애플리케이션의 연결 오류와 재시도 횟수, 연결 풀 사용률과 지연, NLB의 TCP_ELB_Reset_Count를 함께 봅니다.

TCP_ELB_Reset_Count는 NLB가 생성한 RST 패킷 수를 집계하는 CloudWatch 지표라서 AWS CLI로 바로 조회합니다.

AWS CLI - CloudWatch GetMetricStatistics
# --start-time의 date -d는 GNU 문법입니다. macOS/BSD에서는 date -u -v-1H 사용
aws cloudwatch get-metric-statistics \
  --namespace AWS/NetworkELB \
  --metric-name TCP_ELB_Reset_Count \
  --dimensions Name=LoadBalancer,Value=net/my-nlb/xxxxxxxxxxxxxxxx \
  --statistics Sum \
  --period 300 \
  --start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

이 값이 평소 수준을 넘어 계속 높아지면 유휴 타임아웃을 넘긴 연결로 트래픽이 유입되고 있다는 신호이므로 keepalive 적용 상태와 타임아웃 정렬부터 점검합니다. 클라이언트와 타깃이 보낸 RST는 TCP_Client_Reset_Count, TCP_Target_Reset_Count로 구분해 확인합니다.

06상황별 조치 요약

지금 무엇을 해야 하는지는 운영 중인 세대 구성에 따라 갈립니다.

표 5. 운영 상황별 권장 조치
상황필요 조치
Nitro V5 이하만 운영 즉시 조치는 필요 없습니다. V5는 기존 타임아웃 동작을 유지합니다. 다만 지금 keepalive를 넣어 두면 이후 이관해도 아무 일도 일어나지 않습니다.
V5와 V6 혼재 양쪽에 명시적 타임아웃을 설정하거나 keepalive를 적용해 기본값을 무관하게 만듭니다. 별도 노드풀 분리는 필요 없습니다.
V6 이관 예정, 자동 이관 가능 keepalive(프로브 240초 이하)를 먼저 구현하고, 적용할 수 없는 연결은 시작 템플릿에 명시적 ENI 타임아웃을 넣습니다. 이어서 애플리케이션, ENI, 로드 밸런서, NAT 게이트웨이의 정렬을 검증하고 카나리나 블루그린으로 점진 배포하며 연결 오류율을 모니터링합니다.

07검증 시 주의 사항

지속 부하 테스트만으로는 이 문제가 재현되지 않습니다. 현실적인 유휴 구간을 포함한 테스트가 필요하고 아래 문제들은 수 시간을 운영해야 비로소 드러납니다.

  • 연결 풀 동작이 시간이 지나며 드리프트합니다.
  • 연결 10개에서 동작하던 재시도와 백오프 로직이 10,000개에서는 실패합니다.
  • 파일 디스크립터, 메모리, 연결 추적 엔트리 같은 리소스가 임계치를 넘을 때까지 축적됩니다.
  • 계층 간 타임아웃 상호작용은 지속적인 부하 아래에서만 표면화됩니다.

새 인스턴스 세대는 항상 대표성 있는 워크로드로 미리 테스트하고 카나리나 블루그린으로 점진 배포해 문제가 생기면 빠르게 롤백할 수 있게 준비합니다.

08마치며

글머리의 세 가지 증상으로 돌아가 보겠습니다. 주말 직후 첫 요청이 집중 실패하는 것은 350초를 넘긴 유휴 연결이 silent drop으로 끊긴 뒤 연결 풀이 죽은 연결을 재사용했기 때문입니다. 노드 교체 후에 시작된 연결 오류는 Karpenter나 속성 기반 선택이 워크로드를 Nitro V6로 옮기면서 기본값이 바뀐 결과일 가능성이 높습니다. 부하 테스트에서 재현되지 않는 것도 자연스러운 일입니다. 트래픽이 계속 흐르면 유휴 상태가 만들어지지 않기 때문입니다.

350초 기본값은 연결 추적 고갈이라는 더 큰 장애를 막기 위한 조정이고 SO_KEEPALIVE까지 포함해 keepalive를 제대로 적용하면 Nitro 세대와 무관하게 해결됩니다. ENI 타임아웃을 5일로 되돌리는 대응은 원래의 할당량 고갈 문제를 다시 불러오므로 권장하지 않습니다.

복원력 있는 구성은 세 계층에서 함께 동작합니다.

  1. 애플리케이션 계층 - TCP keepalive나 하트비트를 구현합니다.
  2. 인프라 계층 - 워크로드에 맞는 ENI 타임아웃을 설정하며 상위 로드 밸런서, NAT 게이트웨이와의 정렬을 검증합니다.
  3. 모니터링 계층 - conntrack_allowance_available과 conntrack_allowance_exceeded, 연결 오류율을 함께 추적합니다.

이렇게 갖춰 두면 어떤 Nitro 세대, 어떤 인프라 계층에서도 애플리케이션이 동일하게 안정적으로 동작합니다.

인터랙티브 아키텍처 맵 전체 이미지 - 시스템 구성 요소와 흐름을 한 화면으로 보여줍니다
그림 2. 인터랙티브 아키텍처 맵 전체 보기. 이미지를 클릭하면 노드 탐색, 경로 추적, 다크/라이트 테마를 지원하는 인터랙티브 버전 ↗이 열립니다.

--참고 자료

본문에서 인용한 수치와 판단의 근거가 되는 문서들입니다.

핵심 출처

AWS 공식 문서

관련 블로그

외부 자료

EC2 Networking / Technical Advisory

Understanding and Responding to the TCP Connection Tracking Idle Timeout Change on Amazon EC2 Nitro V6 Instances

The default TCP established idle timeout of security group connection tracking became shorter on Nitro V6 instances. This post covers what changed, which workloads are affected and why, and how to prepare with TCP keepalive at the center.

01. Written as of - 2026-08-29

02. Audience - Nitro V6 instances (such as m8i and r8i)

03. Changed parameter - TcpEstablishedTimeout in ConnectionTrackingSpecification

04. Primary sources - the AWS Networking & Content Delivery blog (2026-05-22) and the official EC2 documentation

TL;DR

A service that saw no traffic all weekend fails only its first requests on Monday morning with timeouts. Ever since Karpenter replaced a node, unexplained connection errors have increased, yet no amount of load testing reproduces them. If you have seen these symptoms recently, there is something to check before your application code: the instance generation your workload runs on.

On Nitro V6-based instances shipping since June 2025 (such as m8i and r8i), the default TCP established idle timeout of security group connection tracking dropped from 432,000 seconds (5 days) to 350 seconds. In May 2026 the AWS Networking & Content Delivery blog published Best Practices for TCP Connection Management on EC2, laying out the background of this change and the recommended responses. Based on that blog and the official EC2 documentation, this post walks through what changed, which workloads are affected and why, and how to prepare with TCP keepalive at the center.

01What Changed

What changed is a single default: the TCP established idle timeout, a per-ENI (elastic network interface) setting. In the API you specify it with the TcpEstablishedTimeout parameter inside a network interface's ConnectionTrackingSpecification. The configurable range of 60 to 432,000 seconds is unchanged; on Nitro V6 only the default dropped to 350 seconds.

Table 1. TCP established idle timeout specification
ItemValue
Configurable range60 to 432,000 seconds (5 days)
Default - Nitro V6350 seconds (except P6e-GB200)
Default - all other generations432,000 seconds (5 days), including P6e-GB200
Configuration unitENI
Configuration pathsAWS CLI, launch templates, the console, AWS CloudFormation, Terraform
AWS recommendationSet it lower than 432,000 seconds

The 350-second default does not apply to P6e-GB200, which keeps the same 432,000 seconds as other generations. Note that the Nitro version classification of this type differs across documents. The Nitro instances documentation classifies it as Nitro v5, while the connection tracking documentation lists it as the 350-second exception among Nitro V6 types. The "except" note in Table 1 follows the connection tracking documentation. Either way, the timeout conclusion is the same.

When judging applicability, keep in mind that generation numbers and Nitro versions do not always match. The Graviton4-based 8th-generation M8g, C8g, and R8g are Nitro v5 and unaffected by this change, while the 9th-generation M9g and C9g are Nitro v6 and affected. Check whether your types apply against the list in the Nitro instances documentation. The list below covers virtualized instances.

Table 2. Nitro v6 virtualized instance types (per the Nitro instances documentation)
CategoryNitro v6 instance types
General purposeM8a, M8azn, M8gb, M8gn, M8i, M8id, M8i-flex, M8in, M8idn, M8ine, M8ib, M8idb, M9g, M9gd
Compute optimizedC8a, C8gb, C8gn, C8i, C8id, C8i-flex, C8in, C8ine, C8ib, C9g, C9gd
Memory optimizedR8a, R8gb, R8gn, R8i, R8id, R8i-flex, R8in, R8idn, R8ib, R8idb, X8aedz, X8i
Storage optimizedI8ge
Accelerated computingG7, G7e, P6-B200, P6-B300
HPCHpc8a

The bare metal lineup is this list minus the -flex families, M8ine, C8ine, accelerated computing, and HPC. Easily confused Nitro v5 types are M8g, M8gd, C7gn, C8g, C8gd, R8g, R8gd, X8g, I7ie, I8g, P5en, P6e-GB200, Trn2, Trn2u, and Hpc7g.

UDP has always been short. UdpTimeout, applied to one-way or single request-response flows, defaults to 30 seconds (60 max), and UdpStreamTimeout, applied to flows classified as streams, defaults to 180 seconds (60 min), so UDP is not part of this change.

Terminology - this is not kernel conntrack

The connection tracking discussed here is the Nitro-layer capability that backs security groups, VPC Flow Logs, and network metering, a different layer from the Linux kernel's conntrack. Coincidentally, the kernel parameter nf_conntrack_tcp_timeout_established also defaults to 432,000 seconds, which makes them easy to confuse - but this change lives at the ENI / hypervisor layer, unrelated to the kernel.

No amount of sysctl tuning inside the instance changes this timeout.

02Background of the Change

2.1 Preventing connection tracking allowance exhaustion

The connection tracking allowance is finite, bounded by the Nitro resources available to the instance. Under the old 5-day default, idle and orphaned connections that never received a FIN or RST could sit in the tracking table for up to 5 days each. When they accumulate and exhaust the allowance, the instance cannot establish any new connections until existing entries close or time out. The typical symptoms are connection timeout and connection refused errors, 504 responses from load balancers caused by failed backend connection establishment, and an increase in the Network Load Balancer (NLB) TCP_ELB_Reset_Count metric.

Non-TCP flows such as UDP have 30-to-180-second timeouts and are unlikely to accumulate at scale. The accumulation risk was effectively concentrated in the TCP established state, and this change mitigates exactly that issue.

2.2 Alignment with other networking services

NLB, NAT Gateway, and Gateway Load Balancer already use a 350-second idle timeout. If only the EC2 ENI keeps 5 days, you get half-open states where one side believes the connection is alive while the other has already discarded it. The 350-second default is a choice to reduce this cross-layer mismatch.

Note - the configurable option has existed since November 2023

Adjusting the timeout per ENI is not itself a new capability. The configurable connection tracking idle timeout was introduced in November 2023, allowing 60 to 432,000 seconds on every Nitro generation. What changed this time is less the capability than the V6 default.

03Impact Analysis

3.1 The tricky failure mode silent drop creates

When a connection tracking entry is removed by the timeout, no FIN or RST is delivered to either side. The client socket remains in the ESTABLISHED state, and the moment it sends its next request, the packets are silently discarded. The AWS blog calls this phenomenon silent drop.

There are reasons silent drop is hard to deal with. The application receives no immediate error and hangs until its own timeout fires, so request latency spikes into tens of seconds. Connection pools judge dead connections healthy and keep reusing them, producing a pattern where the first requests right after a long-idle stretch - early mornings or weekends - fail in bursts. Reproduction is hard too: load tests keep traffic flowing, so the idle condition never forms and the tests simply pass.

3.2 Affected workloads

Any connection that can sit idle longer than 350 seconds deserves review.

  • Database connection pools - where maxIdleTime is greater than 350 seconds or unset; the most common victim.
  • IoT telemetry - intermittently transmitting MQTT and long-lived sessions.
  • Persistent HTTP and gRPC connections between microservices - pools reused via keep-alive.
  • WebSockets and long polling - affected when the application heartbeat interval exceeds 350 seconds.
  • Workers with long batch intervals, and SSH or jump-box sessions left open idle

3.3 Unintended generation migration

The most important caution in this change is that migration happens automatically, not by decision. AWS explicitly warns about this in the blog.

Warning - what AWS explicitly calls out in the blog

EC2 Auto Scaling groups using Karpenter or attribute-based instance type selection pick the latest matching generation on their own. Your workload lands on Nitro V6 without you changing anything, and the first sign is unexplained connection failures after a node refresh or scaling event.

In an Amazon EKS environment there are two more things to check. If a Karpenter NodePool has no instance constraints, Nitro V6 types such as m8i can be selected. A generation constraint like karpenter.k8s.aws/instance-generation alone cannot control the Nitro version here: the 9th-generation M9g is Nitro v6 and passes the filter, while the Nitro v5 M8g gets excluded unnecessarily. If you need control, pin an instance family list, and put the fundamental fix in keepalive, which works regardless of generation. Also, in the Amazon VPC CNI default mode, pods share the node's ENIs, so the node ENI's timeout applies to pods as is.

3.4 Cross-layer timeout mismatch

If you raised the NLB idle timeout to 1,800 seconds but the ENI is at 350 seconds, EC2 discards its tracking first while the load balancer keeps the connection. From the client's point of view, a connection that was working suddenly has problems. Adjusting only the ENI timeout creates exactly this trap. Aligning the whole stack is covered in the mitigations section.

04Mitigations

These are in priority order. Apply the first priority, keepalive, and the rest become complementary; skip it, and the remaining measures alone can hardly guarantee safety in a mixed-generation environment.

4.1 Apply TCP keepalive

Keepalive periodically sends probe packets on idle connections so that the connection never reaches an idle state at any infrastructure layer. Because it makes the timeout values themselves irrelevant, it is effective at every layer - EC2, load balancers, and NAT Gateway included - and behavior does not change even with mixed Nitro generations.

With a 350-second ENI timeout, configure probes to start before 240 seconds to leave headroom. 240 seconds leaves 110 seconds of margin before 350. Even if the remote host is already unresponsive and every probe fails, the socket is cleaned up in 240 + 60 × 3 = 420 seconds. The Linux kernel's default keepalive start time is 7,200 seconds (2 hours) - far too long to beat cloud infrastructure timeouts. Tuning the values is mandatory.

/etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 240    # start probes after 4 idle minutes
net.ipv4.tcp_keepalive_intvl = 60    # probe every 60 seconds
net.ipv4.tcp_keepalive_probes = 3    # close the connection after 3 failures
Warning - the most commonly missed trap, SO_KEEPALIVE

The sysctl above only sets probe timing; unless SO_KEEPALIVE is enabled per socket, no probes go out at all. On most operating systems this socket option defaults to off. If you add the sysctl and call keepalive done, the symptoms simply come back - it must also be enabled at the application or driver level.

In code that handles sockets directly, the options look like this in Python.

Application code - Python socket options
import socket

# TCP_KEEPIDLE-family constants are Linux-only (macOS uses TCP_KEEPALIVE; Windows has none)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 240)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)

Adding the TCP_KEEPIDLE-family options lets you override the sysctl defaults per socket. Major applications each have their own keepalive settings.

Table 3. Keepalive settings in major applications
TargetParameters
PostgreSQL tcp_keepalives_idle, tcp_keepalives_interval, tcp_keepalives_count
Redis tcp-keepalive
HTTP clients, messaging libraries Most provide their own keepalive options; check per driver
Windows KeepAliveTime and KeepAliveInterval under HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters

Redis's tcp-keepalive defaults to 300 seconds - inside the 350-second window, but with only 50 seconds of margin. Explicitly lowering it further is the safer play.

On Amazon EKS, specify the net.ipv4.tcp_keepalive_* values through the pod's securityContext.sysctls.

Kubernetes Pod spec - securityContext.sysctls
spec:
  securityContext:
    sysctls:
      - name: net.ipv4.tcp_keepalive_time
        value: "240"
      - name: net.ipv4.tcp_keepalive_intvl
        value: "60"
      - name: net.ipv4.tcp_keepalive_probes
        value: "3"

These sysctls were promoted to safe sysctls in Kubernetes 1.29, so clusters below 1.29 need the unsafe-sysctl allowlist configured. Regardless of version, pods with hostNetwork: true are not allowed to set net.* sysctls and will fail to start. In both cases the answer is applying it node-wide via node userData or a DaemonSet.

One limitation is worth knowing. TCP keepalive defeats infrastructure timeouts, but it cannot detect a remote application that has become unresponsive while the TCP connection stays intact. That is why many workloads run keepalive together with an application-level heartbeat.

4.2 Set the ENI timeout explicitly

If a connection cannot use keepalive and needs idle periods longer than 350 seconds, do not rely on the default - state the value explicitly. In the AWS CLI, use the ModifyNetworkInterfaceAttribute operation.

AWS CLI - ModifyNetworkInterfaceAttribute
aws ec2 modify-network-interface-attribute \
  --network-interface-id eni-xxxxxxxx \
  --connection-tracking-specification TcpEstablishedTimeout=3600

The same value can be set through launch templates, the console, AWS CloudFormation, and Terraform. To apply it to new instances it must go into the launch template; the Karpenter and Auto Scaling group paths are the same. Note that Karpenter's EC2NodeClass may not expose this setting directly - check whether your version supports it first, and if not, going with a custom launch template or the keepalive path is the reliable route.

4.3 Clean up connection lifecycles

Tuning pool settings alongside reduces conflicts with the timeout in the first place. Set maxIdleTime below 350 seconds, rotate connections periodically with maxLifetime, and verify liveness before use with a validation query or health check. Also shrink pool sizes to actual concurrency needs. In HikariCP, for example, the two values look like this.

HikariCP settings - application.properties
# HikariCP: evict at 300s idle (below 350s), rotate connections every 30 minutes
idleTimeout=300000
maxLifetime=1800000

Names differ per pool - the MongoDB driver has maxIdleTimeMS, the R2DBC pool has maxIdleTime - so find and apply the matching setting.

Principle: a connection should be either active with keepalive or closed - never left idle indefinitely. If a long idle period is expected, close it and reopen when needed.

4.4 Align timeouts across the whole stack

The structure AWS recommends is the application closing first. For example, with NLB at 1,800 seconds, ENI at 350, and the application at 60 - shorter timeouts on inner layers - the application closes idle connections first, and the termination signal (FIN) propagates outward through the ENI and the load balancer. No infrastructure layer silently discards a connection. Reverse the order and you get silent drops. Behind an Application Load Balancer (ALB), the default idle timeout is 60 seconds, so connections usually get cleaned up before the ENI. If you raised it (up to 4,000 seconds), verify the ordering against the ENI's 350 seconds has not flipped.

TIMEOUT ALIGNMENT NETWORK LOAD BALANCER Idle timeout 1,800s EC2 ENI CONNECTION TRACKING Idle timeout 350s APPLICATION Idle timeout 60s Closes idle connections first close() → FIN When the inner layer closes first, the termination signal propagates outward. Reversed order causes silent drops. FIN PROPAGATION Application 60s < EC2 ENI 350s < NLB 1,800s
Figure 1. Shorter timeouts on inner layers. Only the order of application 60s, ENI 350s, NLB 1,800s lets the termination signal propagate outward. (Recommended arrangement per Best Practices for TCP Connection Management on EC2, 2026-05-22)

This is why documenting every timeout value in your architecture pays off - the application, load balancers, NAT Gateway, and EC2 ENI. The most reliable method is implementing keepalive shorter than the shortest timeout in the chain, sidestepping the alignment problem entirely.

There are also cases where lowering the timeout is the right move. For services that cannot control client behavior - public APIs, database servers - keeping the ENI timeout at the 350-second default or lower is correct, because idle client connections accumulating and exhausting the allowance is the greater risk.

05Detection and Monitoring

You can check the state directly through the connection tracking metrics exposed by the ENA driver. The interface name differs by AMI - eth0 on Amazon Linux 2, ens5 on AL2023 and other predictable-naming systems - so confirm the name first.

Run inside the instance
ip -br link                       # confirm the interface name
ethtool -S eth0 | grep conntrack  # swap in ens5 if that is the name

The two metrics read as follows; a sharp change in value is itself the signal.

Table 4. Connection-tracking ENA metrics
MetricMeaningInterpretation
conntrack_allowance_available Remaining tracking capacity A sudden large jump means many connections were cleaned up at once - the signature of a timeout event.
conntrack_allowance_exceeded Packets dropped due to allowance exhaustion A spike means new-connection failures are in progress.

Of the two, conntrack_allowance_available is a network performance metric added in 2023 and requires Linux ENA driver 2.8.1 or later. On an old AMI, check the driver version first with ethtool -i eth0.

In production, publish both metrics by adding an ethtool section to the CloudWatch unified agent (1.246396.0 or later) configuration.

CloudWatch agent configuration - amazon-cloudwatch-agent.json
{
  "metrics": {
    "metrics_collected": {
      "ethtool": {
        "interface_include": ["eth0"],
        "metrics_include": [
          "conntrack_allowance_available",
          "conntrack_allowance_exceeded"
        ]
      }
    }
  }
}

Published metrics carry an ethtool_ prefix. When building graphs and alarms, look for the name ethtool_conntrack_allowance_available. If the interface is ens5, adjust interface_include to match. Watch these alongside application connection errors and retry counts, pool utilization and latency, and the NLB TCP_ELB_Reset_Count.

TCP_ELB_Reset_Count is a CloudWatch metric counting RST packets generated by the NLB, so you can query it directly with the AWS CLI.

AWS CLI - CloudWatch GetMetricStatistics
# date -d in --start-time is GNU syntax; on macOS/BSD use date -u -v-1H
aws cloudwatch get-metric-statistics \
  --namespace AWS/NetworkELB \
  --metric-name TCP_ELB_Reset_Count \
  --dimensions Name=LoadBalancer,Value=net/my-nlb/xxxxxxxxxxxxxxxx \
  --statistics Sum \
  --period 300 \
  --start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

If this value keeps climbing past its usual level, traffic is flowing into connections that outlived the idle timeout - start by checking keepalive coverage and timeout alignment. RSTs sent by clients and targets are tracked separately as TCP_Client_Reset_Count and TCP_Target_Reset_Count.

06Action Summary by Scenario

What to do right now depends on the generation mix you operate.

Table 5. Recommended actions by operating scenario
ScenarioRequired action
Nitro V5 or earlier only No immediate action needed. V5 keeps the existing timeout behavior. That said, adding keepalive now means nothing happens when you migrate later.
V5 and V6 mixed Set explicit timeouts on both, or apply keepalive to make the defaults irrelevant. Separate node pools are not needed.
V6 migration planned, or automatic migration possible Implement keepalive first (probes at 240 seconds or less), and put explicit ENI timeouts in launch templates for connections that cannot use it. Then verify alignment across the application, ENI, load balancers, and NAT Gateway, roll out gradually with canary or blue/green, and monitor connection error rates.

07Validation Caveats

Sustained load testing alone does not reproduce this problem. You need tests that include realistic idle periods, and the issues below only surface after hours of operation.

  • Connection pool behavior drifts over time.
  • Retry and backoff logic that worked at 10 connections fails at 10,000.
  • Resources - file descriptors, memory, connection tracking entries - accumulate until they cross thresholds.
  • Cross-layer timeout interactions only surface under sustained load.

Always test new instance generations in advance with representative workloads, and roll out gradually with canary or blue/green so you can roll back quickly if problems appear.

08Closing Thoughts

Back to the three symptoms from the opening. First requests failing in bursts right after a weekend happen because idle connections that crossed 350 seconds were cut by silent drop, and the pool then reused the dead connections. Connection errors that began after node replacement are most likely the default changing as Karpenter or attribute-based selection moved the workload onto Nitro V6. And failing to reproduce it in load tests is natural: with traffic continuously flowing, the idle state never forms.

The 350-second default is an adjustment that prevents the larger failure of connection tracking exhaustion, and applying keepalive properly - SO_KEEPALIVE included - resolves it regardless of Nitro generation. Reverting the ENI timeout to 5 days brings back the original allowance exhaustion problem and is not recommended.

A resilient configuration works at three layers together.

  1. Application layer - implement TCP keepalive or heartbeats.
  2. Infrastructure layer - set ENI timeouts appropriate to the workload, and verify alignment with upstream load balancers and NAT Gateway.
  3. Monitoring layer - track conntrack_allowance_available, conntrack_allowance_exceeded, and connection error rates together.

Set up this way, your application behaves identically and reliably on any Nitro generation and at any infrastructure layer.

Full image of the interactive architecture map - system components and flows in one view
Figure 2. Full view of the interactive architecture map. Click the image to open the interactive version ↗ with node search, route tracing, and dark/light themes.

--References

The documents behind the figures and judgments quoted in this post.

Primary source

Official AWS documentation

Related blogs

External resources