Представьте: два часа ночи, вы деплоите фикс — а сервис не поднимается: Address already in use. Гуглите, находите PID, делаете kill -9. Помогает. Но утром та же ошибка на том же порту.Всё потому, что «порт занят» — это на самом деле два разных диагноза с разными решениями, а на форумах их валят в кучу. В одном случае порт держит живой процесс, и kill действительно помогает. В другом процесс давно мёртв, а порт висит в TIME_WAIT — и тут kill бесполезен, лечится одной строкой в коде.Разбираем оба случая: быстрый фикс под Linux, macOS, Windows и Docker — и что на самом деле происходит с портом, когда процесса уже нет. Понять, при чём тут TIME_WAIT →
Уровень сложностиСредний
Время на прочтение12 мин
Охват и читатели4.2K
Туториал
Представьте: два часа ночи, вы деплоите фикс, а сервис не поднимается: Address already in use. Гуглите, находите PID, kill -9. Это помогает. Но утром ошибка повторяется на том же порту.
Тут и выясняется, что «порт занят» — это два разных диагноза с разными механизмами. Первый — порт правда держит живой процесс, и kill тут действительно помогает. Второй — процесс мёртв уже часов десять, а порт всё равно занят, потому что закрытое TCP-соединение зависает в состоянии TIME_WAIT. Так протокол оберегает новое соединение от заблудившихся пакетов старого. Убивать здесь уже некого, поэтому kill не помогает вообще.
Кому-то kill -9 помогает, кому-то нет — потому что за одним текстом ошибки прячутся два разных случая, а на форумах их валят в кучу. Расскажем, как сделать кросс-платформенный фикс, чтобы прямо сейчас поднять сервис (Linux, macOS, Windows и отдельно Docker). Во второй части статьи разберём, что менять в коде, чтобы проблема не повторялась.
Чтобы практика была из реального прода, а не из теории, материал проверил эксперт:

Сетевой инженер отдела инфраструктуры и эксплуатации в Нетологии
За ошибкой 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() слушающего сокета сервера.
Сколько реально висит порт, зависит от системы:
Система | Длительность | Источник |
Стандарт (RFC 9293) | 2×MSL, при рекомендованном MSL 2 минуты — до 4 минут | |
Linux | Фиксировано около 60 секунд, зашито в ядре, | исходники ядра, include/net/tcp.h |
Windows | По умолчанию 240 секунд, диапазон 30–300 | Microsoft Learn, параметр |
Если 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 с теми же флагами — результат будет тот же.
Здесь 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, отличается только раскладка колонок.
Windowsnetstat -ano | findstr :PORT возвращает:
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 41207PID — последняя колонка. Тот же результат даёт PowerShell:
Get-NetTCPConnection -LocalPort PORT
LocalAddress LocalPort State OwningProcess
0.0.0.0 8080 Listen 41207OwningProcess здесь — то же самое, что PID в netstat, просто под другим названием колонки.
ОС | Команда | Что показывает |
Linux |
| Процесс, PID, состояние |
Linux (запасной вариант) |
| То же, если |
macOS |
| Процесс и PID во второй колонке |
Windows |
| PID в последней колонке |
Windows (PowerShell) |
| То же самое под именем |
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 не пытается поднять то, что вы только что остановили, и порт освобождается предсказуемо.

Сетевой инженер отдела инфраструктуры и эксплуатации в Нетологии
Даже правильный путь через 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.serviceWindowsМораль:
systemctl stop <unit>— правильный путь, но если юнит устроен как пара socket+service, порт стоит в первую очередь проверить на живой процесс, прежде чем списыватьAddress already in useнаTIME_WAIT.
Логика та же — сначала штатное завершение:
Stop-Process -Id <pid>Силовое — только если процесс не отвечает:
taskkill /PID <pid> /FПроверка результата — та же команда поиска, что и на шаге 1, порт должен пропасть из вывода.
Если после любого из этих действий порт всё ещё занят, а PID больше не находится ни одной командой — это TIME_WAIT, вторая причина из начала статьи.
У этого случая та же причина, что и выше, — живой процесс держит порт. Отличаются только сценарий, в котором вы его встретите, и текст самой ошибки. Возникает он так: контейнер запускается с опубликованным портом через -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 сам становится третьим вариантом диагноза «порт занят». Проверка — та же команда поиска, порт должен пропасть из вывода.

DevOps-инженер PRO — 3 проекта в облаке ещё на учёбе
Разворачивайте контейнеры, стройте CI/CD, настраивайте мониторинг и балансировку. Нужен Linux на уровне админа и хотя бы один язык.
Правильное лечение — поставить флаг SO_REUSEADDR на сокете сервера до того, как он попытается забиндиться на порт.
Флаг ставится через 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).
Здесь есть нюанс, который легко упустить при первом чтении документации. Чтобы его найти, нужно собрать минимальный сервер на 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-сокет от старого запуска флага не имел.
SO_REUSEPORT — не альтернативное название SO_REUSEADDR, а другая возможность: несколько сокетов слушают один порт одновременно, а ядро само распределяет входящие соединения между ними. К TIME_WAIT и к рестартам это отношения не имеет. Задача, для которой он нужен, — разложить нагрузку между несколькими процессами на одном порту, обычно для балансировки в многопроцессных серверах.
На Windows SO_REUSEADDR ведёт себя не так, как на Linux и macOS. Флаг разрешает двум сокетам одновременно занять один и тот же порт. То есть теоретически второй процесс может перехватить порт первого, пока тот ещё работает. За эксклюзивность на Windows отвечает отдельная опция, SO_EXCLUSIVEADDRUSE — она не даёт другому сокету занять порт, пока текущий им владеет. Различие подтверждено документацией Microsoft.
Вывод: код с SO_REUSEADDR, который проверили на Linux или macOS, нельзя просто скопировать на Windows и ожидать того же эффекта — там этот флаг решает другую задачу.
Симптом | Причина | Что делать |
| Порт держит живой процесс — свой или чужой | Найти PID ( |
|
| Проверить напрямую: |
| Порт держит контейнер (обычно через |
|
Проверили состояние сокета напрямую (ss -tan)? Пустой вывод ss -tlnp сам по себе ничего не доказывает.
Не забытый ли контейнер с restart: always прячется за docker-proxy?
SO_REUSEADDR в коде сервиса стоит заранее, а не появился только сейчас, во время инцидента?
«Порт занят» — это всегда одна из трёх ситуаций:
Чужой живой процесс.
Ваш собственный процесс всё ещё висит в TIME_WAIT.
Порт через 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), сделать ИИ-агента — и добавить к стеку востребованное направление. Для действующих разработчиков есть трек без базовых тем.
Если пока хочется просто попробовать формат, начните с бесплатного:
Курса «ИТ в действии: как создаются и живут цифровые продукты» — как раз о том, почему сервисы ломаются в самый неподходящий момент. За 5 дней вы проходите путь продукта в шести ролях, от кода до сети.
Курса «ИИ в деле: ускорьте свою работу» обходится без теории: вы берёте свою рабочую рутину и автоматизируете её нейросетью на 4 проектах.
Вводного курса «Специалист по искусственному интеллекту» открывает демодоступ к платной программе на 5 дней. За это время вы обучаете первую модель в реальных инструментах и смотрите, из чего складывается работа в ИИ.
А чтобы держать знания свежими не только когда прижало, пригодится База знаний Нетологии: это больше 16 000 видеоуроков и вебинаров по ИТ и диджиталу, и до 10 видео в день там можно смотреть бесплатно.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Почему IPv4 закончился, а IPv6 всё ещё не повсюду — взгляд фронтенд-разработчика | 0 | 13.95 | 01-10-2026 |
| 2 | Попасть нельзя промахнуться | 0 | 7.23 | 30-09-2026 |
| 3 | Мы слишком долго пытаемся починить не ту систему Есть странная ... | -1 | 6.11 | 03-10-2026 |
| 4 | Купили двухсокетный сервер на 128 ядер, а база данных стала ... | 0 | 9.49 | 01-10-2026 |
| 5 | Трек на 46 минут долго молчал: отдаём перекодированный звук, пока ffmpeg его ещё пишет | 0 | 6.56 | 30-09-2026 |
| 6 | Часы Unix тикали от розетки: откуда взялся 1970 год и что сломается в 2038-м | 0 | 10.94 | 02-10-2026 |
| 7 | Часы Unix тикали от розетки: откуда взялся 1970 год и что сломается в 2038-м | 0 | 10.94 | 02-10-2026 |
| 8 | Flutter на клиенте, Fletch на сервере: бэкенд на знакомом Dart | 0 | 8.42 | 02-10-2026 |
| 9 | eBPF и XDP - это не просто мода, а будущее ... | -1 | 5.12 | 03-10-2026 |
| 10 | [Перевод] N‑tier, Clean Architecture и Vertical Slice: практическое сравнение для.NET | 0 | 7.99 | 02-10-2026 |