Address already in use: как найти, кто занял порт, и почему он не всегда освобождается сразу. address already in use.. address already in use. DevOps.. address already in use. DevOps. docker.. address already in use. DevOps. docker. kill.. address already in use. DevOps. docker. kill. lsof.. address already in use. DevOps. docker. kill. lsof. netstat.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait. Блог компании Нетология.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait. Блог компании Нетология. порт занят.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait. Блог компании Нетология. порт занят. Программирование.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait. Блог компании Нетология. порт занят. Программирование. Сетевые технологии.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait. Блог компании Нетология. порт занят. Программирование. Сетевые технологии. Системное администрирование.. address already in use. DevOps. docker. kill. lsof. netstat. so_reuseaddr. ss. time_wait. Блог компании Нетология. порт занят. Программирование. Сетевые технологии. Системное администрирование. сокеты.

Представьте: два часа ночи, вы деплоите фикс, а сервис не поднимается: Address already in use. Гуглите, находите PID, kill -9. Это помогает. Но утром ошибка повторяется на том же порту.

Тут и выясняется, что «порт занят» — это два разных диагноза с разными механизмами. Первый — порт правда держит живой процесс, и kill тут действительно помогает. Второй — процесс мёртв уже часов десять, а порт всё равно занят, потому что закрытое TCP-соединение зависает в состоянии TIME_WAIT. Так протокол оберегает новое соединение от заблудившихся пакетов старого. Убивать здесь уже некого, поэтому kill не помогает вообще.

Кому-то kill -9 помогает, кому-то нет — потому что за одним текстом ошибки прячутся два разных случая, а на форумах их валят в кучу. Расскажем, как сделать кросс-платформенный фикс, чтобы прямо сейчас поднять сервис (Linux, macOS, Windows и отдельно Docker). Во второй части статьи разберём, что менять в коде, чтобы проблема не повторялась.

Чтобы практика была из реального прода, а не из теории, материал проверил эксперт:

Address already in use: как найти, кто занял порт, и почему он не всегда освобождается сразу - 1

Евгений Иванов

Сетевой инженер отдела инфраструктуры и эксплуатации в Нетологии

TL;DR

За ошибкой Address already in use стоят две разные причины, и лечатся они по-разному.

Если порт слушает живой процесс — свой или чужой — ищем PID (ss/lsof/netstat, на Windows — netstat/Get-NetTCPConnection) и останавливаем: сначала мягко (kill/Stop-Process), жёстко — только если мягкое не сработало. Юниты под systemd останавливаем через systemctl stop, напрямую их лучше не трогать.

Если процесс уже мёртв, а порт всё равно занят — это TIME_WAIT, обычное поведение TCP после закрытия соединения (на Linux держится около 60 секунд, на Windows — до 240). kill здесь бесполезен: чинится флагом SO_REUSEADDR на сокете сервера, поставленным до bind() — в Python вручную, в Go почти всегда за вас.

Отдельно стоит Docker: ошибка выглядит иначе (port is already allocated), порт на хосте в дефолтной конфигурации держит служебный docker-proxy, а решение — просто остановить контейнер.

И ловушка для тех, кто пишет под несколько платформ: у SO_REUSEADDR на Windows другая, более рискованная семантика, чем на Linux и macOS — копировать код между системами без проверки не стоит.

Почему «порт занят» — это две разные ошибки

За текстом Address already in use прячутся две ситуации с разными причинами.

Первая — порт слушает чужой живой процесс. Кто-то занял его раньше вас: другой инстанс сервиса, забытый процесс с прошлого запуска, системная служба. Здесь находим процесс, останавливаем, и порт свободен.

Вторая — та самая история с ночным деплоем из начала статьи. TIME_WAIT возникает у стороны, которая закрыла соединение первой, и держится, чтобы поздний пакет от старого соединения не попал в новое, если оно откроется на том же порту слишком быстро. При перезапуске сервера прежние принятые соединения оставляют порт в этом состоянии, и bind падает с той же ошибкой, хотя живого процесса уже нет. Это фича протокола, не баг.

Один-два TIME_WAIT после рестарта — норма, лечится флагом в коде. Его разберём в разделе о профилактике. Тысячи TIME_WAIT под нагрузкой на проде — признак исчерпания портов. Это отдельная тема тюнинга, здесь её не будет — но если ваш прод уже упирается в такие вещи, значит, пора разбираться с эксплуатацией системно. Курс «DevOps-инженер PRO» как раз про это: Linux на уровне админа, контейнеры, CI/CD, мониторинг, балансировка.

Если в поисках решения натыкаетесь на совет покрутить net.ipv4.tcp_tw_reuse или tcp_tw_recycle (в новых ядрах tcp_tw_recycle вообще убрали) — это не то лечение. Эти настройки ядра управляют повторным использованием TIME_WAIT-сокетов для исходящих соединений, когда ваш код сам — клиент. Эта статья — про bind() слушающего сокета сервера.

Сколько реально висит порт, зависит от системы:

Система

Длительность TIME_WAIT

Источник

Стандарт (RFC 9293)

2×MSL, при рекомендованном MSL 2 минуты — до 4 минут

RFC 9293

Linux

Фиксировано около 60 секунд, зашито в ядре, sysctl не меняется

исходники ядра, include/net/tcp.h

Windows

По умолчанию 240 секунд, диапазон 30–300

Microsoft Learn, параметр TcpTimedWaitDelay

Если ss, lsof или netstat находят слушающий процесс на порту — это первый случай, когда процесс нужно найти и остановить. Но если команда вроде ss -tlnp возвращает пустоту, это ничего не доказывает. Такие команды по построению показывают только LISTEN-сокеты и физически не покажут TIME_WAIT, даже если бы вы очень хотели. Не гадайте по методу исключения, а посмотрите на TIME_WAIT прямо: ss -tan | grep :PORT (или точнее ss -tan state time-wait) покажет состояние сокета явным текстом.

ss или lsof без прав администратора для чужих процессов иногда показывают сам факт LISTEN, но без PID. Это третий, отдельный случай — не пустой вывод и не TIME_WAIT, — и решается он через sudo.

Быстрый фикс: кто занял порт

Разберем, что делать в каждом случае, по шагам и командам под три системы.

Шаг 1. Найти процесс, который слушает порт

Linux

Запускаем ss -tlnp | grep :PORT и получаем примерно такую строку:

LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=41207,fd=19))

LISTEN означает, что порт слушает входящие соединения, а не занят под что-то другое. В скобках после users — имя процесса и его PID, здесь node с pid=41207. Этот PID понадобится на следующем шаге.

ss — рекомендуемый инструмент, но встречается не везде. На урезанных Docker-образах и минимальных виртуалках пакет iproute2 иногда не установлен по умолчанию, и команды просто нет в системе. В этом случае берём lsof -i :PORT или fuser PORT/tcp — оба покажут тот же PID, просто в другом формате вывода.

Отдельно про netstat: команда встречается в старых ответах на форумах, но уже несколько лет как считается устаревшей, пакет net-tools не входит в дистрибутивы по умолчанию. Если видите совет с netstat -tlnp в старой статье, на Linux замените на ss с теми же флагами — результат будет тот же.

macOS

Здесь netstat PID не показывает вообще — приходится сразу идти через lsof, флаги немного другие:

lsof -nP -iTCP:PORT -sTCP:LISTEN

Вывод выглядит так:

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME

node 41207 user 21u IPv4 0x1a2b3c 0t0 TCP *:8080 (LISTEN)

PID — во второй колонке, 41207. Логика та же, что на Linux, отличается только раскладка колонок.

Windows

netstat -ano | findstr :PORT возвращает:

Proto Local Address Foreign Address State PID

TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 41207

PID — последняя колонка. Тот же результат даёт PowerShell:

Get-NetTCPConnection -LocalPort PORT

LocalAddress LocalPort State OwningProcess

0.0.0.0 8080 Listen 41207

OwningProcess здесь — то же самое, что PID в netstat, просто под другим названием колонки.

ОС

Команда

Что показывает

Linux

ss -tlnp | grep :PORT

Процесс, PID, состояние LISTEN

Linux (запасной вариант)

lsof -i :PORT / fuser PORT/tcp

То же, если ss недоступен

macOS

lsof -nP -iTCP:PORT -sTCP:LISTEN

Процесс и PID во второй колонке

Windows

netstat -ano | findstr :PORT

PID в последней колонке

Windows (PowerShell)

Get-NetTCPConnection -LocalPort PORT

То же самое под именем OwningProcess

Шаг 2. Освободить порт правильно

PID найден. Дальше — как остановить процесс так, чтобы порт освободился, а не завис в переходном состоянии.

Linux и macOS

Обе системы понимают одну и ту же команду kill, потому что обе — родня по семейству Unix. Начинаем с kill <PID> — сигнал SIGTERM, штатное завершение. Процесс получает команду закрыться сам: закрывает сокеты и файлы, снимает блокировки. Проверяем результат той же командой, которой искали процесс на шаге 1 — ss, lsof или netstat по тому же порту. Пустой вывод означает, что порт свободен.

Если процесс не откликнулся спустя несколько секунд, отправляем более жёсткий сигнал: kill -9 <PID>, он же SIGKILL. Перехватить его нельзя, процесс завершится принудительно и без собственной очистки: может остаться блокировка файла или осиротевший дочерний процесс. Поэтому пробуем сначала SIGTERM, а SIGKILL держим на случай, когда мягкое завершение не подействовало. Проверка та же — повторяем поиск по порту.

Отдельная ситуация на Linux — если процесс запущен как сервис под управлением systemd. Systemd — менеджер процессов, он есть почти в каждом современном дистрибутиве Linux. Обычно он запускает фоновые сервисы вроде nginx или postgresql и следит за ними; каждый такой сервис называется юнитом. Если убить такой процесс напрямую через kill, systemd увидит, что его юнит неожиданно исчез, и в зависимости от настроек может сам перезапустить сервис — тогда порт снова окажется занятым, и вы не поймёте почему. Поэтому для таких сервисов правильно останавливать через сам systemd:

systemctl stop <unit>

Так systemd не пытается поднять то, что вы только что остановили, и порт освобождается предсказуемо.

Address already in use: как найти, кто занял порт, и почему он не всегда освобождается сразу - 2

Евгений Иванов

Сетевой инженер отдела инфраструктуры и эксплуатации в Нетологии

Даже правильный путь через systemd — не железная гарантия. На Ubuntu, начиная с версии 22.10, SSH по умолчанию переведён на socket activation: порт слушает не сам sshd, а отдельный юнит ssh.socket, который при входящем подключении передаёт его ssh.service. На Ubuntu 24.04 в проде встречалась ситуация, когда при рестарте старый процесс sshd не завершался до конца, хотя юнит формально остановился:

ssh.service: Unit process 10908 (sshd) remains running after unit stopped.
ssh.service: Found left-over process 10908 (sshd) in control group while starting unit.
ssh.socket: Failed to create listening socket (0.0.0.0:22): Address already in use

ssh.socket пытается забиндить порт 22, порт уже занят тем самым зависшим процессом, оба юнита падают, и машина перестаёт отвечать по SSH. Проблема плавающая, лечилась вручную — найти и добить оставшийся PID. Рабочим воркэраундом стал откат на классическую схему без socket activation:

systemctl disable --now ssh.socket
systemctl enable --now ssh.service

Мораль: systemctl stop <unit> — правильный путь, но если юнит устроен как пара socket+service, порт стоит в первую очередь проверить на живой процесс, прежде чем списывать Address already in use на TIME_WAIT.

Windows

Логика та же — сначала штатное завершение:

Stop-Process -Id <pid>

Силовое — только если процесс не отвечает:

taskkill /PID <pid> /F

Проверка результата — та же команда поиска, что и на шаге 1, порт должен пропасть из вывода.

Если после любого из этих действий порт всё ещё занят, а PID больше не находится ни одной командой — это TIME_WAIT, вторая причина из начала статьи.

Отдельный случай: порт держит Docker

У этого случая та же причина, что и выше, — живой процесс держит порт. Отличаются только сценарий, в котором вы его встретите, и текст самой ошибки. Возникает он так: контейнер запускается с опубликованным портом через -p, например, docker run -p 8080:8080 myapp.

Дальше — один из трёх обычных сценариев: вы останавливаете разработку и забываете про контейнер, он перезапускается сам из-за настройки restart, или вы просто пробуете запустить его новую версию заново на том же порту. Docker отказывает, но не текстом Address already in use, а своим собственным: Bind for 0.0.0.0:PORT failed: port is already allocated. Человек ищет решение по этому конкретному тексту ошибки, поэтому для него это выглядит как отдельная проблема, хотя внутри — тот же случай № 1.

Здесь же поджидает вторая неожиданность — в дефолтной конфигурации Docker на bridge-сети. Если запустить ss или lsof по такому порту, среди процессов найдётся не приложение из контейнера, а docker-proxy. Это служебный процесс: по умолчанию Docker пробрасывает порт с хоста внутрь контейнера именно через него (userland-proxy включён из коробки).

Если контейнер работает в host-сети, сетевое пространство общее с хостом — и ss покажет сам процесс внутри контейнера, без прокси. Если же вы просто отключили userland-proxy в настройках демона, docker-proxy тоже не появится, но ss покажет уже не процесс, а голое «кто-то слушает порт» — без опознаваемого посредника. Не зная о docker-proxy, можно потратить время на поиски процесса, которого там нет и не будет — само приложение работает внутри изолированного сетевого пространства контейнера, а на хосте виден только его посредник.

Решение — остановить сам контейнер, который этот процесс обслуживает:

docker ps

docker stop <id>

Docker в норме завершает docker-proxy вслед за контейнером и освобождает порт — но именно эта связка иногда ломается (обычно на IPv6 или в старых версиях), и тогда мёртвый docker-proxy сам становится третьим вариантом диагноза «порт занят». Проверка — та же команда поиска, порт должен пропасть из вывода.

Address already in use: как найти, кто занял порт, и почему он не всегда освобождается сразу - 3

DevOps-инженер PRO — 3 проекта в облаке ещё на учёбе

Разворачивайте контейнеры, стройте CI/CD, настраивайте мониторинг и балансировку. Нужен Linux на уровне админа и хотя бы один язык.

Посмотреть программу →

Профилактика: SO_REUSEADDR в коде, а не kill вслепую

Правильное лечение — поставить флаг SO_REUSEADDR на сокете сервера до того, как он попытается забиндиться на порт.

Python

Флаг ставится через setsockopt, обязательно до bind:

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", 8080))
sock.listen()

Для серверов на основе socketserver (в том числе http.server) то же самое делается через атрибут класса, тоже до создания сервера:

import socketserver

class MyServer(socketserver.TCPServer):
    allow_reuse_address = True

Go: почти ничего добавлять не нужно

В Go можно не искать, куда вставить SO_REUSEADDR для обычного рестарта сервиса — net.Listen ставит этот флаг на каждый TCP-листенер автоматически, на уровне самого рантайма. На Linux и macOS за это отвечает функция setDefaultListenerSockopts в стандартной библиотеке. Разница с Python здесь в том, что для Go этот шаг просто нельзя забыть — библиотека не оставляет такой возможности.

На Windows это правило не действует. В исходниках стандартной библиотеки (sockopt_windows.go) setDefaultListenerSockopts для Windows вообще не трогает SO_REUSEADDR, а в комментарии авторы Go объясняют почему. Windows и так по умолчанию разрешает bind поверх TIME_WAIT-сокета, отдельный флаг для этого не нужен. А ставить SO_REUSEADDR просто за компанию с Linux/macOS было бы вредно — на Windows он означает не «дай перезапуститься», а «пусти два активных сокета на один порт одновременно», с непредсказуемым результатом (см. раздел про Windows ниже). Так что для обычного рестарта сервиса на Windows это уже решено на уровне системы, без единой строчки кода.

Единственный случай, где всё же приходится трогать сокет-опции руками, — SO_REUSEPORT (что это и почему не путать с SO_REUSEADDR — см. ниже). Для него net.Listen уже не хватает: нужен net.ListenConfig с функцией Control, внутри которой вызывается setsockopt с SO_REUSEPORT (например, через golang.org/x/sys/unix).

Linux: флаг нужен на обоих сокетах

Здесь есть нюанс, который легко упустить при первом чтении документации. Чтобы его найти, нужно собрать минимальный сервер на Python, принять и закрыть соединение первым (сервер уходит в TIME_WAIT), затем уронить процесс и попробовать перезапустить его на том же порту. Без SO_REUSEADDR bind закономерно падает с Address already in use. Дальше добавляем флаг только на новый сокет, и bind снова падает с той же ошибкой, как будто флага не было вообще. 

На Linux SO_REUSEADDR срабатывает только тогда, когда флаг стоял и на сокете, который ушёл в TIME_WAIT, и на новом сокете, который пытается занять тот же порт, — именно так и получилось в тесте. Это написано в man 7 socket, но эту строчку часто пролистывают. В сети встречается и версия «флаг старого сокета на самом деле не проверяется, это неточность документации» — со ссылкой на чтение исходников ядра. Тест ставился, чтобы своими глазами увидеть поведение, — и оно совпало с man-страницей. FreeBSD в этом смысле проще: там для рестарта достаточно поставить флаг только на новом сокете.

Что в итоге: если сервис упал со старой версией кода без SO_REUSEADDR, добавление флага не спасает сразу. При немедленном рестарте сервис всё равно один раз упадёт с той же ошибкой: TIME_WAIT-сокет от старого запуска флага не имел. 

Linux: SO_REUSEPORT — не путать с SO_REUSEADDR

SO_REUSEPORT — не альтернативное название SO_REUSEADDR, а другая возможность: несколько сокетов слушают один порт одновременно, а ядро само распределяет входящие соединения между ними. К TIME_WAIT и к рестартам это отношения не имеет. Задача, для которой он нужен, — разложить нагрузку между несколькими процессами на одном порту, обычно для балансировки в многопроцессных серверах.

Windows: другая семантика SO_REUSEADDR

На Windows SO_REUSEADDR ведёт себя не так, как на Linux и macOS. Флаг разрешает двум сокетам одновременно занять один и тот же порт. То есть теоретически второй процесс может перехватить порт первого, пока тот ещё работает. За эксклюзивность на Windows отвечает отдельная опция, SO_EXCLUSIVEADDRUSE — она не даёт другому сокету занять порт, пока текущий им владеет. Различие подтверждено документацией Microsoft. 

Вывод: код с SO_REUSEADDR, который проверили на Linux или macOS, нельзя просто скопировать на Windows и ожидать того же эффекта — там этот флаг решает другую задачу. 

Шпаргалка: симптом → причина → команда

Симптом

Причина

Что делать

Address already in use при запуске, и ss/lsof/netstat находят слушающий процесс

Порт держит живой процесс — свой или чужой

Найти PID (ss/lsof/netstat) → остановить (kill → kill -9, systemctl stop для юнитов, Stop-Process/taskkill /F на Windows)

Address already in use сразу после остановки своего процесса, а ss -tlnp пуст

TIME_WAIT — процесса нет, порт ещё «дозревает»

Проверить напрямую: ss -tan | grep :PORT. Если видите TIME_WAIT — добавить SO_REUSEADDR в код до bind(). Подождать минуту-две — только разовый обход, не решение

Bind for 0.0.0.0:PORT failed: port is already allocated

Порт держит контейнер (обычно через docker-proxy)

docker ps → docker stop <id>

Чек-лист: что проверить, прежде чем паниковать

  • Проверили состояние сокета напрямую (ss -tan)? Пустой вывод ss -tlnp сам по себе ничего не доказывает.

  • Не забытый ли контейнер с restart: always прячется за docker-proxy?

  • SO_REUSEADDR в коде сервиса стоит заранее, а не появился только сейчас, во время инцидента?

Что запомнить

«Порт занят» — это всегда одна из трёх ситуаций: 

  1. Чужой живой процесс. 

  2. Ваш собственный процесс всё ещё висит в TIME_WAIT. 

  3. Порт через docker-proxy держит контейнер. 

Решение для первой и третьей — найти и корректно остановить то, что держит порт: kill → kill -9, systemctl stop для юнитов под systemd, docker stop для контейнеров. Для второй kill не поможет вообще, потому что процесса, который нужно останавливать, уже нет — решение живёт в коде. Это SO_REUSEADDR, поставленный до bind(), и на Linux флаг должен стоять заранее, а не добавляться посреди инцидента: иначе один рестарт всё равно упадёт с той же ошибкой, потому что старый TIME_WAIT-сокет флага не увидел. SO_REUSEPORT и SO_EXCLUSIVEADDRUSE — соседи по названию, но не по задаче, путать их с SO_REUSEADDR не стоит.

Если сомневаетесь, какая перед вами ситуация, самый надёжный способ проверить — посмотреть на состояние сокета напрямую. ss -tan | grep :PORT покажет TIME_WAIT текстом, без интерпретаций.


❯ Куда расти дальше

Если всё выше вы знали и так — значит, с эксплуатацией у вас порядок. А «порт занят» — далеко не последняя засада в эксплуатации. Если хочется не тушить пожары вслепую, а расти, вот куда:

  • Если сервис падает от каждого рестарта или захлёбывается под нагрузкой, курс «Go-разработчик PRO» выводит на другой уровень — учит строить высоконагруженные сервисы, которые держат нагрузку и не ломаются на ровном месте. Он для специалистов с опытом, с отдельным треком для сисадминов и DevOps.

  • Если половина времени уходит на рутину — тесты, документацию, однотипный код, — курс «Нейросети для разработчиков» учит снимать её нейросетями и писать код быстрее и чище. По итогам — рабочий навык и удостоверение о повышении квалификации.

  • Если языковые модели уже повсюду, а вы пока только пользуетесь чужими, курс «LLM-разработчик» помогает собрать своё — дообучить модель, подключить поиск по документам (RAG), сделать ИИ-агента — и добавить к стеку востребованное направление. Для действующих разработчиков есть трек без базовых тем.

Если пока хочется просто попробовать формат, начните с бесплатного:

А чтобы держать знания свежими не только когда прижало, пригодится База знаний Нетологии: это больше 16 000 видеоуроков и вебинаров по ИТ и диджиталу, и до 10 видео в день там можно смотреть бесплатно.

Автор: Alrighty

Источник