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
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.
# Dinleyen (LISTEN) bütün TCP portlarılsof -nP -iTCP -sTCP:LISTEN # Belirli bir portu kim tutuyorlsof -i :3000Buradaki 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.
ps -p <PID> -o commandLinux
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.
# Dinleyen bütün TCP/UDP soketleri, process bilgisiylesudo ss -tulpnBayrakları 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:
# Belirli bir portu kim tutuyorlsof -i :3000 # 3000/tcp'yi tutan process, sadece PID dönerfuser 3000/tcpWarning
-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.
# 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).OwningProcessUfak 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:
:: 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
# 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.
# lsof -ti sadece PID basar, doğrudan kill'e verilebilirkill $(lsof -ti :3000) # fuser ile: 3000/tcp'yi tutan process'leri kapatfuser -k 3000/tcpBirden ç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.
for port in 3000 5173 8080; do pid=$(lsof -ti :$port) if [ -n "$pid" ]; then echo "Port $port temizleniyor (PID: $pid)" kill $pid fidoneWindows
# 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: bütün node process'lerini kapatırStop-Process -Name node # cmd: bütün node.exe process'lerini kapatırtaskkill /IM node.exe /FError
İş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:
pid=$(lsof -ti :3000)if [ -n "$pid" ]; then kill "$pid"else echo "3000 portunda dinleyen process yok"fiBir 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.
# 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:
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:
docker ps # 3000'i hangi konteyner yayinliyor, bakdocker stop <container_id> # konteyneri durdur, port serbest kalsinWSL2: 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.PORTkullan, ç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-portgibi paketler ya da Vite'ınstrictPort: falseayarı, 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
| Belirti | Muhtemel sebep | Çözüm |
|---|---|---|
| Port listede yok ama bind olmuyor | Process IPv6 (::) üzerinde dinliyor | lsof -i6 :PORT ya da filtresiz lsof -i :PORT |
| lsof boş döndü ama uygulama patlıyor | Portu başka kullanıcı/root tutuyor | sudo lsof -i :PORT ile tekrar dene |
| Hiçbir process görünmüyor ama port dolu | Docker konteyneri yayınlıyor | docker ps + docker stop |
| Kapatıp açınca hep EADDRINUSE | TIME_WAIT bekliyor | Birkaç saniye bekle ya da SO_REUSEADDR aç |
| kill çalıştı ama port hâlâ dolu | Ortada kalmış çocuk process var | Doğru PID'i bul: lsof -i :PORT, ata sürece bak |
Warning
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.

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.

