Skip to main content
15 min read

Port Zaten Kullanımda Hatası: Portu Bulma ve Güvenle Kapatma

Mustafa Kürşad BaşerMustafa Kürşad BAŞER
Port Zaten Kullanımda Hatası: Portu Bulma ve Güvenle Kapatma

Yazılımla uğraşan herkesin defalarca gördüğü bir ekran vardır. Projeyi çalıştırmak istersin ve terminal o tanıdık cümleyi geri yollar. Error: listen EADDRINUSE: address already in use. Port dolu, uygulama ayağa kalkmıyor ve devam edebilmek için önce bunu çözmen gerekiyor.

Çoğumuzun ilk hamlesi kill -9 olur. Çalışır da. Ama bu hamlenin iki ayrı sıkıntısı var. Biri, çoğu zaman gereğinden sert kaçıyor, process'e toparlanıp düzgünce kapanma şansı bile tanımıyorsun. Diğeri daha sinsi, neyi neden öldürdüğünü bilmiyorsun, o yüzden bir dahakine yine aynı yere geliyorsun. Bu yazıda portların nasıl çalıştığına, "kullanımda" hatasının altında gerçekte ne döndüğüne, hangi platformda hangi aracın doğru olduğuna ve bir process'i nasıl güvenli şekilde kapatacağına bakacağız. macOS, Linux ve Windows, üçü için de.

Bir process'i öldürmeden önce ne öldürdüğünü bilmek

IP adresi makineyi gösterir. Ama tek makinede aynı anda bir sürü servis koşar. Web sunucusu, veritabanı, SSH, Redis, üstüne üç tane dev sunucusu. Hepsi aynı IP'yi paylaşır. Gelen trafiğin hangisine ait olduğunu ayıran şey port, yani 0 ile 65535 arasında 16 bitlik bir sayı. İşin güzel tarafı, bir bağlantı tek başına portla değil dört şeyle tanımlanıyor. Bunlar kaynak IP, kaynak port, hedef IP ve hedef port. Binlerce kişinin aynı anda 443 portundaki aynı siteye bağlanabilmesi de bu yüzden, herkesin kaynak portu farklı.

TCP ile UDP aynı portu paylaşmaz

İki taşıma protokolü var ve port uzayları ayrı. Yani 3000/TCP ile 3000/UDP iki ayrı dünya, ikisi aynı anda dinlenebilir. TCP bağlantı kurar, el sıkışır, veriyi sıralı ve garantili taşır. Web, veritabanları, SSH hep bunun üzerinde. UDP ise paketi atar ve arkasına bakmaz. DNS, video, oyun gibi gecikmenin can yaktığı yerlerin işi. Dev sunucularının neredeyse tamamı TCP konuştuğu için örneklerin çoğu da TCP üzerinden gidecek.

O "state" sütunu aslında çok şey söylüyor

netstat ya da ss çıktısındaki durum sütununu çoğu kişi atlıyor, oysa sorunun yarısı orada yazıyor. En sık karşılaşacakların:

  • LISTEN: Bir process o portta bağlantı bekliyor. Dev sunucun ayaktayken port bu durumda olur.
  • ESTABLISHED: Kurulmuş, çalışan bir bağlantı var, veri akıyor.
  • TIME_WAIT: Bağlantı kapandı ama işletim sistemi soketi birkaç saniye (pratikte 30 ila 120 saniye) elinde tutuyor. Amacı, ağda geç kalmış paketlerin yeni bir bağlantıya karışmaması. Bağlantıyı kapatan tarafta oluşur. Sunucuyu kapatıp hemen açınca "port hâlâ dolu" demesinin bir numaralı sebebi tam olarak budur.
  • CLOSE_WAIT: Karşı taraf kapatmış ama senin uygulaman soketi henüz close() ile kapatmamış. Bunlardan bir sürü birikiyorsa neredeyse kesin: kodunda bir yerde bağlantı kapatılmıyor, yani soket sızıntın var.

EADDRINUSE tek bir şey demek değil

Çoğu kişi bu hatayı "başka bir program portu tutuyor" diye okuyor. Bazen öyle, ama altından bambaşka şeyler de çıkabiliyor. Hangisi olduğunu bilirsen çözümü de doğru seçersin:

  • Gerçekten ayakta bir process portu dinliyordur. En klasiği: bir önceki sunucuyu kapatmayı unutmuşsundur.
  • TIME_WAIT'te bekleyen eski bir bağlantı vardır. Process ölmüştür ama işletim sistemi soketi henüz bırakmamıştır. Birkaç saniye bekle, geçer.
  • SO_REUSEADDR olmadan hızlı yeniden bind denemişsindir. TIME_WAIT'teki bir adrese bu seçenek açık değilse tekrar bind edemezsin.
  • Crash sonrası ortada kalmış bir process vardır. Ana süreç patlamış ama onun başlattığı çocuk (mesela nodemon'un node'u) hâlâ ayakta ve portu tutuyordur.
  • Portu başka bir kullanıcı ya da root tutuyordur. Senin yetkinle koşan araç onu hiç göstermeyebilir.

IPv4, IPv6 ve "boş görünüp dolu çıkan" portlar

Çok kişinin epey vakit kaybettiği bir durum bu. Portu listeliyorsun, ekranda hiçbir şey yok ama bind etmeye kalkınca yine "kullanımda" diyor. Neredeyse her seferinde suçlu IPv4/IPv6 ayrımıdır. Kısaca anlatayım. 127.0.0.1 IPv4'ün loopback adresi, ::1 onun IPv6 hâli. 0.0.0.0 bütün IPv4 arayüzleri demek, :: ise bütün IPv6. localhost ise sadece bir isim ve çoğu sistemde hem 127.0.0.1'e hem ::1'e çözülür.

İşin can yakan kısmı şu. Process :: üzerinde, yani IPv6'da dinliyor olabilir ve bazı sistemlerde bu IPv4 bağlantılarını da kapsar. Sen IPv4 filtresiyle arayınca ekran boş gelir ama portu bind edemezsin, çünkü o aslında IPv6 tarafında çoktan dinleniyordur. Çaresi de basit, ya filtresiz ara ya da IPv6'ya bak.

Note

Port "boşmuş gibi duruyor ama bind olmuyorsa" ilk şüphelin IPv6 olsun. lsof -i6 :3000 ya da filtresiz lsof -i :3000 ile bir bak, çoğu zaman olay orada çözülür.

Portu kim tutuyor: port, PID, process zinciri

Hangi platformda olursan ol amaç aynı: porttan PID'e, PID'den process adına, oradan da tam komut satırına ulaşmak. Bir portu körü körüne öldürmeden önce onu kimin tuttuğuna bakmak, baştan savma çözümle düzgün çözümün ayrıldığı yerdir.

macOS

macOS'ta ss yok, işi lsof görüyor. Dinleyen bütün TCP portlarını listelemek ve belirli bir portu kimin tuttuğuna bakmak istersen şu iki komut yeter.

bash
# Dinleyen (LISTEN) bütün TCP portlarılsof -nP -iTCP -sTCP:LISTEN # Belirli bir portu kim tutuyorlsof -i :3000

Buradaki bayraklar boşuna değil. -n, IP'leri DNS ile isme çevirmeye kalkmaz. Bu olmadan lsof ters DNS sorgusuna girer ve takılıp kalabilir, yani hız için neredeyse şart. -P ise portu servis adına çevirmez, 80 yerine "http" yazmaz, sayısal görmek hayatı kolaylaştırır. -iTCP -sTCP:LISTEN de sadece TCP ve sadece dinleyenleri süzer. Bir PID'in arkasındaki tam komutu merak ediyorsan şunu çalıştır.

bash
ps -p <PID> -o command

Linux

Linux'ta gidilecek yer ss (iproute2 paketinden). Veriyi doğrudan kernelden Netlink üzerinden çeker, binlerce bağlantının olduğu makinelerde netstat'tan gözle görülür kadar hızlıdır. netstat ise artık eski net-tools paketinin parçası, bir sürü dağıtımda varsayılan olarak kurulu bile gelmiyor.

bash
# Dinleyen bütün TCP/UDP soketleri, process bilgisiylesudo ss -tulpn

Bayrakları bir kere oturtursan komutu ezberlemen gerekmez: -t TCP, -u UDP, -l sadece dinleyenler, -p soketi kullanan process (PID ve ad), -n sayısal gösterim, yani isim çözmeye uğraşma. Eski makinelerde sudo netstat -tulpn da aynı işi görür. Alternatifler:

bash
# Belirli bir portu kim tutuyorlsof -i :3000 # 3000/tcp'yi tutan process, sadece PID dönerfuser 3000/tcp

Warning

Process bilgisini (-p) ve başka kullanıcılara ait PID'leri görmek için çoğu zaman sudo gerekir. sudo olmadan ss/netstat çalışır ama process sütununu bomboş bırakabilir. "Port dolu ama hiçbir şey görünmüyor" diyorsan, ilk işin sudo ile tekrar denemek olsun.

Windows

Windows'ta iki yol var. Modern olanı PowerShell. Get-NetTCPConnection sana PID yerine OwningProcess, yani portu tutan sürecin PID'sini verir. Onu Get-Process ile eşleyince okunur bir bilgiye dönüşür.

powershell
# Dinleyen bütün portlarGet-NetTCPConnection -State Listen # Belirli porttaki bağlantılarGet-NetTCPConnection -LocalPort 3000 # Tek satırda: 3000'i tutan processGet-Process -Id (Get-NetTCPConnection -LocalPort 3000).OwningProcess

Ufak bir tuzak: Get-NetTCPConnection ile Get-Process arasındaki o kısacık anda process ölebilir, sen de "Cannot find a process with PID" yersin. Garantiye almak için Get-Process çağrısına -ErrorAction SilentlyContinue ekle. Daha klasik takılıyorsan cmd her Windows'ta hazır:

bash
:: 3000 portunu içeren satırlar (en sağdaki sayı PID'dir)netstat -ano | findstr :3000 :: O PID hangi uygulamaymış, bakalımtasklist /FI "PID eq <PID>"

netstat -ano bayraklarına gelince, -a bütün bağlantılar, -n sayısal, -o sahip process'in PID'si demek. En sağdaki sütun PID oluyor, onu tasklist ile bir isme bağlıyorsun.

Process'i kapatmak: önce nazikçe, sonra zorla

Mantık tek cümle: process'e önce düzgün kapanma şansı ver, olmuyorsa zorla. kill -9 refleksinin baş belası olması da tam bu adımı atlamasından.

macOS ve Linux

bash
# Nazik kapatma: SIGTERM gider, process toparlanıp kapanabilirkill <PID> # Zorla kapatma: SIGKILL, process hiçbir şey yapamadan anında düşerkill -9 <PID>

Argümansız kill <PID> aslında SIGTERM (15. sinyal) gönderir. Bu, process'e "topla toparlan, kapan" demek. Açık dosyalarını kapatır, veritabanı transaction'ını bitirir, geçici dosyalarını siler. kill -9 ise SIGKILL, yani yakalanamaz, process tek kelime edemeden düşer. Sonu da çoğu zaman bozuk veri dosyaları, açık kalmış lock dosyaları, yarım yazılmış kayıtlar olur. O yüzden -9 ilk hamle değil, son çare. Portu tutanı tek komutla devirmek için şunu kullan.

bash
# lsof -ti sadece PID basar, doğrudan kill'e verilebilirkill $(lsof -ti :3000) # fuser ile: 3000/tcp'yi tutan process'leri kapatfuser -k 3000/tcp

Birden çok dev portunu tek seferde toparlayacaksan döngü en derli toplu yol. Dikkat: her port için önce PID'i değişkene alıp boş mu diye bakıyoruz, körlemesine kill'e vermiyoruz.

bash
for port in 3000 5173 8080; do  pid=$(lsof -ti :$port)  if [ -n "$pid" ]; then    echo "Port $port temizleniyor (PID: $pid)"    kill $pid  fidone

Windows

powershell
# PowerShell: nazik kapatmaStop-Process -Id <PID> # PowerShell: zorla kapatmaStop-Process -Id <PID> -Force # cmd: belirli PID'i zorla kapattaskkill /PID <PID> /F

İsimle toplu kapatma da var ama buradan sonrası ayağını denk almayı gerektiriyor:

powershell
# PowerShell: bütün node process'lerini kapatırStop-Process -Name node # cmd: bütün node.exe process'lerini kapatırtaskkill /IM node.exe /F

Error

İsimle toplu kapatma, ada uyan ne varsa hepsini götürür. Bugünün makinesinde VS Code'un dil sunucusu, başka sekmelerdeki dev sunucular, Electron uygulamaları derken kolayca 10'dan fazla node process'i koşuyor olabilir. Hepsi birden gider. Bu komutu ancak ne yaptığından eminken kullan.

İşi bir üst seviyeye taşıyan ayrıntılar

Komutları biliyorsun artık. Geriye, zamanla öğrenilen birkaç pratik ayrıntı kalıyor. Önemli olanları aşağıda topladım.

kill $(lsof -ti :3000) deyiminin sinsi tarafı

Çok pratik bir kalıp, ama bir tuzağı var. Porta bağlı process yoksa lsof -ti boş döner ve komut argümansız kill haline gelir. En hafifinden hata alırsın. Doğrusu, PID'i önce değişkene alıp boş mu diye bakmak:

bash
pid=$(lsof -ti :3000)if [ -n "$pid" ]; then  kill "$pid"else  echo "3000 portunda dinleyen process yok"fi

Bir kere yaz, ömür boyu kullan

Her seferinde o uzun komutları yazmak yerine kabuğuna kalıcı birer fonksiyon koy. Aşağıdaki killport önce SIGTERM dener, process inat edip ayakta kalırsa SIGKILL'e geçer, yani doğru sırayı senin yerine uyguluyor. whoport ise öldürmeden önce kimin tuttuğunu gösteriyor. ~/.zshrc ya da ~/.bashrc dosyana ekle.

bash
# Portu tutan process'i güvenle kapat (önce SIGTERM, sonra SIGKILL)killport() {  if [ -z "$1" ]; then    echo "Kullanim: killport <port>"    return 1  fi  local pid  pid=$(lsof -ti :"$1")  if [ -z "$pid" ]; then    echo "Port $1 zaten bos"    return 0  fi  echo "Port $1 -> PID $pid kapatiliyor"  kill "$pid" 2>/dev/null  sleep 2  if kill -0 "$pid" 2>/dev/null; then    echo "Hala ayakta, SIGKILL gonderiliyor"    kill -9 "$pid"  fi} # Portu kim tutuyor, oldurmeden gosterwhoport() {  if [ -z "$1" ]; then    echo "Kullanim: whoport <port>"    return 1  fi  lsof -nP -i :"$1"}

Bundan sonra killport 3000 demen yeter. PowerShell tarafındaysan aynı kolaylığı $PROFILE dosyana taşıyabilirsin:

powershell
function Kill-Port {    param([Parameter(Mandatory)][int]$Port)    $conn = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue    if (-not $conn) {        Write-Host "Port $Port zaten bos"        return    }    $procId = $conn.OwningProcess    $proc = Get-Process -Id $procId -ErrorAction SilentlyContinue    Write-Host "Port $Port -> PID $procId ($($proc.ProcessName)) kapatiliyor"    Stop-Process -Id $procId -Force}

pkill -f node ve killall node neden tehlikeli?

pkill -f node, komut satırında "node" geçen ne varsa hepsini kapatır. -f tam komut satırında arama yaptığı için, yolunda "node_modules" geçen alakasız bir aracı bile yanlışlıkla yakalayabilir. killall node ise adı tam "node" olan bütün process'leri götürür. İkisi de editör eklentilerini, öbür projelerinin dev sunucularını, arka planda dönen araçları silip süpürebilir. Ne zaman makul? Kendi makinende, ortada tek bir node projesi olduğundan eminken. Ortak sunucuda ya da CI'da asla.

Docker'ın tuttuğu portu öldürmeye çalışma

Bir host portunu Docker konteyneri yayınlıyorsa (mesela -p 3000:3000), lsof sana gerçek uygulamayı değil docker-proxy'yi ya da Docker'ın arka plan sürecini gösterir. Onu kapatmaya çalışmak Docker'ı bozar, portu da boşa çıkarmaz. Doğrusu konteyneri durdurmak:

bash
docker ps                    # 3000'i hangi konteyner yayinliyor, bakdocker stop <container_id>   # konteyneri durdur, port serbest kalsin

WSL2: iki tarafa birden bakmak gerek

WSL2'de port işi iki katmanlı. WSL içinde ayağa kaldırdığın bir servise Windows tarafından localhost üzerinden erişilir (localhost forwarding). Ama Windows tarafındaki bir servisle WSL içindeki bir servis aynı portu isterse çakışırlar ve tek taraftan bakınca sebebini göremezsin. Portu hem WSL içinde (ss -tulpn) hem Windows tarafında (Get-NetTCPConnection) ayrı ayrı kontrol et. Forwarding bazen takılır, wsl --shutdown ile WSL'i baştan başlatmak çoğu zaman sıfırlayıp düzeltir.

Asıl çözüm: portu sürekli öldürmek zorunda kalmamak

Şu girişteki hikâyeye dönelim. Ben her gün port öldürüyorsam, mesele portta değil, benim kurulumumda. Port öldürmek ateşi düşürüyor ama hastalığı geçirmiyor. Birkaç kalıcı çözüm var, hepsini bir öğleden sonra ayarlayıp aylarca unutabilirsin:

  • Portu .env'den oku. Koda gömmek yerine process.env.PORT kullan, çakışınca tek satır değiştirip geçersin.
  • Boş portu otomatik buldur. Node'da server.listen(0) işletim sisteminden boş bir port ister. detect-port gibi paketler ya da Vite'ın strictPort: false ayarı, port doluysa bir sonrakini kendi dener.
  • Graceful shutdown yaz. SIGTERM'i yakalayıp soketleri ve bağlantıları düzgünce kapatan kod, "ortada kalmış process portu tutuyor" derdini en baştan kökünden keser.

1024 altı portlar neden sudo istiyor?

Well-known portlar (0-1023) tarihsel olarak ayrıcalıklıdır. Mantığı şöyle. 80 ya da 443'e bind olabilen bir program, kullanıcıların güvendiği standart bir servisi taklit edebilir. O yüzden işletim sistemi bu portlara bind etmeyi root/yönetici yetkisine bağlamış. Geliştirmede 80 yerine 3000, 5173, 8080 kullanmamızın sebebi de bu zaten, sudo istemezler. (Linux'ta setcap ya da authbind ile root olmadan da düşük porta bind edilebilir ama o ayrı, daha ileri bir konu.)

TIME_WAIT mi beklemeli, SO_REUSEADDR mı açmalı?

Sunucuyu kapatıp hemen açınca EADDRINUSE alıyorsan ve process gerçekten ölmüşse, sebep büyük ihtimalle TIME_WAIT. İki seçeneğin var. Birincisi, birkaç saniye beklemek. İşletim sistemi soketi bırakınca dert kendiliğinden geçer, en zararsız yol da bu. İkincisi, soketi oluştururken SO_REUSEADDR açmak. Böylece TIME_WAIT'teki bir adrese yeniden bind edebilirsin ve modern framework'lerin çoğu (Express dahil) bunu zaten açıyor. Bir de şunu karıştırma, SO_REUSEPORT başka bir şey. Aynı porta birden çok process'in aynı anda bind olmasına ve kernel'in yükü paylaştırmasına izin verir, o bir yük dağıtma senaryosudur.

Sık takılınan yerler ve hızlı teşhis

BelirtiMuhtemel sebepÇözüm
Port listede yok ama bind olmuyorProcess IPv6 (::) üzerinde dinliyorlsof -i6 :PORT ya da filtresiz lsof -i :PORT
lsof boş döndü ama uygulama patlıyorPortu başka kullanıcı/root tutuyorsudo lsof -i :PORT ile tekrar dene
Hiçbir process görünmüyor ama port doluDocker konteyneri yayınlıyordocker ps + docker stop
Kapatıp açınca hep EADDRINUSETIME_WAIT bekliyorBirkaç saniye bekle ya da SO_REUSEADDR aç
kill çalıştı ama port hâlâ doluOrtada kalmış çocuk process varDoğru PID'i bul: lsof -i :PORT, ata sürece bak

Warning

Bir process'i kapatmadan önce ne olduğunu mutlaka teyit et: ps -p <PID> -o command (macOS/Linux) ya da Get-Process -Id <PID> (Windows). PID'i elle yazarken bir rakamı şaşırıp prod veritabanını ya da bütün editör oturumunu uçurmak, başa gelmeyecek bir şey değil.

Sonuç

En kalıcı çözüm aslında basit. killport ile whoport'u kabuğuna ekle, dev sunucunda otomatik boş port seçmeyi aç (Vite'ta strictPort: false yeter), bir de portunu .env'den okut. Bu üçünü bir kere ayarladığında EADDRINUSE çoğu zaman seni hiç durdurmaz, port doluysa uygulama bir sonrakine geçer ve sen farkında bile olmazsın.

Yine de o satırı gördüğün gün, aklında üç ihtimal olsun yeter. Ya bir process hâlâ ayakta, ya TIME_WAIT işini bitirmemiş, ya da IPv4 ile IPv6 birbirine girmiş. Hangisiyse doğru komutu yazıp geçersin. kill -9'u büsbütün yasaklamıyorum tabii, herkesin elinin altında dursun. Yalnızca ilk değil, son hamlen olsun.

Share this post

Link copied!
Mustafa Kürşad Başer
Author

Mustafa Kürşad Başer

Senior Software Engineer

A passionate software engineer who enjoys creating elegant solutions to complex problems. Beyond coding, I am deeply interested in exploring the intersections of technology, art, and human consciousness.