TIME_WAIT
은 TCP 통신에서 연결을 종료한 쪽(보통 클라이언트) 이 마지막으로 ACK
응답을 보내고 난 뒤 일정 시간 동안 그 연결 정보를 메모리에 보관하며 대기하는 상태를 의미한다. 이 상태는 TCP의 신뢰성 보장 원칙 때문에 반드시 필요한 과정이며 단순히 소켓을 닫았다고 해서 네트워크 상의 연결이 즉시 사라지는 것이 아니다.
TCP는 데이터의 정확한 순서 보장, 손실 없는 전송, 중복 방지 등의 특성을 제공한다. 이런 특성들을 유지하기 위해 연결을 끊는 과정에서도 마지막 패킷이 제대로 도달했는지 확인하고 지연된 패킷이나 재전송 요청에 대응할 준비를 해야 한다.
예를 들어 클라이언트가 마지막 ACK
패킷을 서버로 전송했는데 해당 패킷이 중간에서 유실되면 서버는 재전송을 시도할 수 있다. 이때 클라이언트가 이미 연결 정보를 지워버렸다면 해당 재전송에 응답할 수 없게 되며 TCP의 신뢰성 보장도 깨지게 된다. 그래서 2 * MSL
(Maximum Segment Lifetime) 시간 동안 클라이언트는 TIME_WAIT
상태로 대기하며 패킷 재전송에 응답할 준비를 유지하는 것이다.
TCP는 연결을 끊는 과정에서도 4-way handshake라는 정해진 절차를 따른다. 이 과정에서 마지막 ACK
를 보낸 측이 TIME_WAIT
상태로 진입하게 된다.
FIN
전송 (연결 종료 요청)ACK
응답 (요청 수락)FIN
전송 (서버도 종료 의사 전달)ACK
응답 (서버 종료 확인)TIME_WAIT
상태 진입 (지연 패킷 대비 대기)클라이언트가 보낸 마지막 ACK
가 중간에 손실되었을 경우를 대비해 서버는 일정 시간 내에 해당 ACK
가 오지 않으면 자신이 보냈던 FIN
패킷을 다시 전송한다. 이때 클라이언트가 응답할 수 있어야 TCP의 종료 절차가 완전히 마무리된다. 또한, 이전 연결에서 떠돌던 지연된 패킷이 새로운 연결에 혼입되는 문제를 방지하기 위해 클라이언트는 MSL의 2배 시간 동안 기다리게 된다.
서버 또는 클라이언트가 아주 짧은 시간 동안 수많은 TCP 연결을 맺고 끊는 경우 TIME_WAIT
상태가 빠르게 누적되어 시스템 자원이 고갈되는 문제가 발생할 수 있다. 특히 웹 서버나 API 서버처럼 다수의 클라이언트가 빠르게 요청을 보내고 응답을 받은 뒤 연결을 종료하는 구조에서는 더욱 심각하게 나타난다.
예시 시나리오
TIME_WAIT
상태로 유지되고 있어 같은 포트를 재사용할 수 없다.이로 인해 다음과 같은 문제가 발생한다.
Connection refused
, BindException: Address already in use
오류실제로 netstat
, ss
등의 명령어를 사용해 보면 TIME_WAIT
상태의 소켓이 수천 개 이상 잡혀있는 것을 확인할 수 있다.
# TIME_WAIT 상태 확인
netstat -an | grep TIME_WAIT | wc -l
SO_REUSEADDR
, SO_REUSEPORT
옵션 사용소켓을 열 때 TIME_WAIT
상태에서도 포트를 재사용할 수 있도록 설정하는 옵션이다.
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
SO_REUSEADDR
: TIME_WAIT
중에도 포트를 바인딩 가능하게 함SO_REUSEPORT
: 다수의 소켓이 같은 포트 번호로 바인딩 가능 (Linux 커널 3.9+)⚠️ 주의: 포트를 재사용하면 이전 연결의 잔류 패킷이 새 연결로 들어올 위험이 있으므로 신중하게 사용해야 한다.
HTTP 1.0 시절에는 요청마다 TCP 연결을 새로 맺고 끊는 구조였기 때문에 TIME_WAIT
이 매우 자주 발생했다. 하지만 HTTP/1.1부터는 기본적으로 Connection: keep-alive
옵션이 적용되어 하나의 TCP 연결을 여러 요청에서 재사용하도록 개선되었다.
Connection: keep-alive
TIME_WAIT
상태도 자연스럽게 감소예: Nginx의 keepalive_timeout, Apache의 KeepAliveTimeout 설정 등
TCP 종료는 FIN
을 먼저 보낸 쪽이 마지막 ACK
를 보내므로 TIME_WAIT
에 빠진다. 따라서 클라이언트가 먼저 FIN
을 보내게 하면 서버는 마지막 ACK
만 보내게 되어 TIME_WAIT
상태에 빠지지 않는다. 서버에서 연결 종료를 지연하거나 애플리케이션 레벨에서 클라이언트가 먼저 close()
하도록 유도하는 설계
운영 서버에서는 커널 파라미터를 조정해 TIME_WAIT
상태의 지속 시간을 줄이거나 재사용을 허용할 수 있다.
# TIME_WAIT 유지 시간 조절
sysctl -w net.ipv4.tcp_fin_timeout=30
# TIME_WAIT 상태의 포트 재사용 허용
sysctl -w net.ipv4.tcp_tw_reuse=1
# 빠른 회수 허용 (NAT 환경에서는 권장하지 않음
sysctl -w net.ipv4.tcp_tw_recycle=1
tcp_fin_timeout
: TIME_WAIT
상태 유지 시간 (기본 60초 -> 30초)tcp_tw_reuse
: TIME_WAIT 소켓을 재사용할 수 있도록 허용 (주로 클라이언트용)tcp_tw_recycle
: 빠르게 회수하지만 NAT 환경에서 문제 발생 가능TIME_WAIT
은 TCP의 신뢰성 보장을 위한 필수 안전장치다. 하지만 짧은 시간 동안 많은 연결을 맺고 끊는 구조에서는 자원 고갈 및 연결 실패 등 다양한 문제를 유발할 수 있다. 이를 해결하기 위해 상황에 맞게 다음과 같은 방법을 조합해서 사용해야 한다.
SO_REUSEADDR
, SO_REUSEPORT
)tcp_fin_timeout
, tcp_tw_reuse
등)특히 백엔드 서버가 외부 API 호출 등 클라이언트 역할을 수행할 경우 TIME_WAIT
은 서버에 집중적으로 누적되어 장애 원인이 될 수 있으므로 반드시 주의가 필요하다.
이걸 공부하면서 TCP가 얼마나 신중하게 "연결을 끊는지" 알 수 있었다. 처음엔 단순히 socket.close()
만 호출하면 끝나는 줄 알았는데 실제로는 그 뒤에도 수 초에서 수 분간 시스템 내부에서 연결 정보가 살아 있으며 네트워크 상의 모든 예외 상황을 고려해 기다리고 있는 구조였다. 운영 서버에서는 "왜 갑자기 포트가 부족하지?", "연결이 안 돼?" 같은 문제가 종종 발생하는데 알고 보니 원인은 대부분 TIME_WAIT
이었다. 특히 서버가 백엔드 간 통신에서 클라이언트 역할을 병행할 때 이 문제가 부각되며 소켓 재사용이나 keep-alive
미적용 시 심각한 장애로 이어질 수 있었다. 이후 SO_REUSEADDR
, keep-alive
, 커널 파라미터 조정 등을 통해 문제를 해결하며 단순히 애플리케이션 로직이 아닌 네트워크 레벨의 동작과 자원까지 고려한 설계가 얼마나 중요한지 깨닫게 되었다. 앞으로는 시스템을 설계할 때도 기능 구현뿐 아니라 운영 중 발생할 수 있는 연결 문제와 리소스 관리까지 고려한 구조를 만드는 것이 중요하다는 점을 다시 한 번 알 수 있었다.