Trafik yükseldiğinde site birden 502 vermeye başlar, error.log dosyasını açarsınız ve aynı satır saniyede onlarca kez tekrar eder:
2026/08/18 21:14:03 [alert] 1188#1188: accept4() failed (24: Too many open files)
2026/08/18 21:14:03 [crit] 1191#1191: *84213 socket() failed (24: Too many open files) while connecting to upstream
Refleks bellidir: ulimit -n yazarsınız, 1024 görürsünüz, ulimit -n 65535 ile büyütürsünüz, servisi yeniden başlatırsınız. On dakika sonra hata geri gelir. Bu noktada çoğu kişi limitin uygulanmadığını düşünüp /etc/security/limits.conf dosyasına satır ekler, sunucuyu tamamen yeniden başlatır ve hâlâ aynı çıktıyla karşılaşır.
Sebep basit ama sezgiye aykırıdır: systemd tarafından başlatılan bir servis, sizin kabuk oturumunuzun limitini de limits.conf dosyasını da okumaz. O dosya PAM üzerinden yalnızca interaktif giriş yapan oturumlara uygulanır; nginx.service ise PAM'e hiç uğramadan, systemd'nin kendi tanımladığı çerçeveyle doğar. Yani doğru komutu çalıştırıp yanlış katmanı düzeltmiş olursunuz.
Bu yazıda hatanın üç ayrı katmanını sırayla düzelteceğiz, sonra asıl kritik soruya geçeceğiz: limit gerçekten mi yetersiz, yoksa uygulamanız descriptor sızdırdığı için mi tavana çarpıyorsunuz? Limitin ne olduğunu ve ulimit bayraklarını hiç bilmiyorsanız önce ulimit ile kaynak limitleri yazısını okuyun; burada kavramı tekrar anlatmayıp doğrudan teşhis ve kalıcı çözüme odaklanacağım.
Too Many Open Files Hatası Tam Olarak Neyi Söylüyor#
Linux'ta yalnızca disk üzerindeki dosyalar değil, her TCP soketi, her Unix soketi, her pipe, her eventfd ve her inotify örneği de bir file descriptor (dosya tanımlayıcı) tüketir. Nginx bir istemci bağlantısı için bir descriptor, upstream'e (PHP-FPM'e) açtığı bağlantı için bir descriptor daha kullanır. Yani 5.000 eş zamanlı istek, tek başına 10.000 descriptor demektir.
Hata mesajındaki parantez içi sayı, hangi tavana çarptığınızı söyler ve bu ayrım tüm çözümü belirler:
| errno | Sabit | Anlamı | Doğru katman |
|---|---|---|---|
| 24 | EMFILE | Süreç kendi tavanına çarptı | LimitNOFILE / worker_rlimit_nofile |
| 23 | ENFILE | Sistem genelindeki tavan doldu | fs.file-max (sysctl) |
Pratikte gördüğünüz neredeyse her zaman 24'tür; sistem geneli tavan modern çekirdeklerde RAM'e göre otomatik hesaplandığı için 23 çok nadirdir. Bu yüzden fs.file-max değerini büyütmek çoğu senaryoda hiçbir şeyi düzeltmez — ama üçüncü katmanı hazırlayan bir ön koşuldur, birazdan göreceğiz.
Aynı hata metnini üreten ama tamamen farklı bir kaynak daha vardır: inotify. Bir dosya izleme aracı (inotifywait, bazı deploy scriptleri, Node tabanlı watcher'lar) fs.inotify.max_user_instances tavanına çarptığında da çekirdek EMFILE döndürür ve ekranda yine "too many open files" yazar. Descriptor sayınız düşükken bu hatayı alıyorsanız önce oraya bakın:
# Kullanıcı başına inotify örneği ve izleme tavanı
sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches
ulimit -n Değiştirdim Ama Hata Devam Ediyor#
ulimit -n 65535 komutunun etkisi, o komutu yazdığınız kabuk oturumu ve o oturumdan başlattığınız süreçlerle sınırlıdır. Oturumu kapattığınızda kaybolur, başka bir SSH oturumuna geçmez ve en önemlisi systemd'nin yönettiği servislere hiç ulaşmaz.
/etc/security/limits.conf de aynı boşluğa düşer. Bu dosyayı okuyan şey PAM'in pam_limits.so modülüdür ve PAM yalnızca bir kullanıcı giriş yaptığında devreye girer: SSH oturumu, su, login. systemctl start nginx böyle bir giriş değildir. Dolayısıyla limits.conf içine yazdığınız nginx soft nofile 65535 satırı, nginx.service için ölü bir satırdır.
Gerçek durumu tek komutla görebilirsiniz. Kabuğunuzun değeri değil, çalışan sürecin değeri bağlayıcıdır:
# Çalışan nginx master sürecinin gerçek limitleri
cat /proc/$(pgrep -o nginx)/limits | grep -i "open files"
# PHP-FPM master için aynı bakış
cat /proc/$(pgrep -o php-fpm)/limits | grep -i "open files"
Çıktıdaki Soft Limit sütunu hâlâ 1024 gösteriyorsa, ne yaptığınızın önemi yok — o süreç 1024 descriptor ile yaşıyor demektir. systemd 240 ve sonrasında servisler için varsayılan DefaultLimitNOFILE=1024:524288'dir: soft limit 1024, hard limit 524288. Yani tavanı yükseltme yetkiniz zaten var, sadece soft limiti kimse yükseltmiyor.
Aşağıdaki üç katmanı sırayla uygulayın. Sıra önemlidir, çünkü her katman bir üstündekinin tanımladığı tavanı aşamaz.
Katman 1: Sistem Geneli fs.file-max ve fs.nr_open#
Bu katmana çoğu zaman dokunmanız gerekmez, ama iki değeri bilmeniz gerekir. fs.file-max sistemin toplam descriptor tavanıdır; fs.nr_open ise tek bir sürecin RLIMIT_NOFILE değerinin alabileceği en büyük sayıdır ve varsayılanı 1048576'dır. LimitNOFILE değerini fs.nr_open üzerine çıkarmaya çalışırsanız servis başlamaz.
Önce mevcut kullanımı ölçün:
# allocated unused max → üçüncü sütun fs.file-max
cat /proc/sys/fs/file-nr
sysctl fs.file-max fs.nr_open
İlk sütun (allocated) üçüncü sütuna yaklaşmıyorsa sistem tavanı sorun değildir; ikinci katmana geçin. Yaklaşıyorsa kalıcı olarak büyütün:
sudo tee /etc/sysctl.d/99-file-max.conf >/dev/null <<'EOF'
fs.file-max = 2097152
EOF
sudo sysctl --system
sysctl -w ile yapılan değişiklik yeniden başlatmada kaybolur; bu yüzden dosyaya yazıp --system ile uygulayın.
Katman 2: systemd Servisi İçin LimitNOFILE Ayarı#
Asıl düzeltme burasıdır. Paket yöneticisinin kurduğu .service dosyasını doğrudan düzenlemeyin — bir sonraki güncelleme üzerine yazar. Bunun yerine drop-in override kullanın:
sudo systemctl edit nginx
Açılan editöre yalnızca şunu yazın:
[Service]
LimitNOFILE=65535
Bu, /etc/systemd/system/nginx.service.d/override.conf dosyasını oluşturur ve paket güncellemelerinden etkilenmez. Aynısını PHP-FPM için de yapın (systemctl edit php8.3-fpm gibi, sürümünüze göre). Tek bir değer yazdığınızda systemd hem soft hem hard limiti o değere sabitler; ayrı ayrı vermek isterseniz LimitNOFILE=65535:524288 biçimini kullanın.
Ardından — ve bu adım en sık atlanan yerdir:
sudo systemctl daemon-reload
sudo systemctl restart nginx
reload değil, restart. RLIMIT_NOFILE bir sürece doğduğu anda verilir; nginx -s reload ya da systemctl reload nginx yalnızca yapılandırmayı tazeler, master süreci yerinde kalır ve eski limitiyle yaşamaya devam eder. Sayfaların düzeldiğini sanıp yükte tekrar hataya düşmenin bir numaralı sebebi budur.
Tüm servisler için varsayılanı topluca yükseltmek isterseniz /etc/systemd/system.conf içindeki DefaultLimitNOFILE= satırını kullanabilirsiniz, ama bunun için sunucuyu yeniden başlatmanız gerekir ve etkisi geniştir. Servis bazlı override çoğu durumda daha doğru tercihtir. systemd unit dosyalarının yapısına aşina değilseniz systemd servis yönetimi yazısı bu dosyaların anatomisini açıklar.
Yeniden başlatmadan, çalışan bir süreç üzerinde acil müdahale gerekiyorsa prlimit kullanabilirsiniz. Kalıcı değildir, sadece yangını söndürür:
sudo prlimit --pid $(pgrep -o nginx) --nofile=65535:65535
Katman 3: Nginx worker_rlimit_nofile ve PHP-FPM rlimit_files#
systemd limiti düzeltti, ama Nginx'in kendi ayarı var. worker_rlimit_nofile direktifi, master sürecin worker'lara devrettiği descriptor limitini belirler ve systemd'nin verdiği hard limite kadar yükselebilir. Ayarlanmadığında worker'lar sistemden miras aldıkları soft limitle çalışır.
Kritik ilişki şudur: bir worker, her istemci bağlantısı için bir, upstream bağlantısı için bir descriptor kullanır. Bu yüzden worker_rlimit_nofile değeri worker_connections değerinin en az iki katı olmalıdır.
# /etc/nginx/nginx.conf — ana (main) bağlam
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 8192;
multi_accept on;
}
Değişiklikten sonra sözdizimini doğrulayıp servisi yeniden başlatın:
sudo nginx -t && sudo systemctl restart nginx
# Worker süreçlerinin gerçekten aldığını doğrula
for p in $(pgrep -f "nginx: worker"); do
grep "Max open files" /proc/$p/limits
done
PHP-FPM tarafında ayarın adı farklıdır ve havuz (pool) dosyasına yazılır, php.ini içine değil:
; /etc/php/8.3/fpm/pool.d/www.conf
rlimit_files = 65535
rlimit_core = 0
PHP-FPM'de descriptor tüketiminin ikinci kaynağı, aynı anda ayakta duran worker sayısıdır. pm.max_children değerini yükseltmek, dolaylı olarak descriptor talebini de yükseltir; ikisini birlikte planlayın. Havuz boyutlandırmasının mantığı için PHP-FPM pool ayarları ve iki servisin nasıl konuştuğu için Nginx ve PHP-FPM yapılandırması yazılarına bakabilirsiniz.
Veritabanı da aynı zincirde yer alır: MySQL/MariaDB için open_files_limit değeri, systemd LimitNOFILE değerinden büyük olamaz. SHOW VARIABLES LIKE 'open_files_limit'; çıktısı yazdığınız değerden düşükse, sınırlayan katman systemd'dir.
Limit mi Yetmiyor Yoksa Descriptor Sızıntısı mı Var#
Buraya kadar yaptığınız her şey tavanı yükseltmekti. Ama tavanı yükselttikçe hata birkaç saat sonra geri geliyorsa, sorun limit değil sızıntıdır: uygulama açtığı descriptor'ları kapatmıyordur ve siz sadece çökme anını erteliyorsunuzdur.
Ayrım için tek bir soru yeterlidir: descriptor sayısı yük ile mi artıyor, yoksa zamanla mı? Yük düştüğünde geri iniyorsa limit sorunudur; yük düşse bile inmiyorsa sızıntıdır.
Ölçmek için sayacı bir süre izleyin:
# PHP-FPM master sürecinin açık descriptor sayısını 5 saniyede bir yazdır
PID=$(pgrep -o php-fpm)
watch -n5 "ls /proc/$PID/fd | wc -l"
# Tüm süreçler için en çok descriptor tutan ilk 10 süreç
sudo lsof -n 2>/dev/null | awk '{print $2}' | sort | uniq -c | sort -rn | head -10
Sayı gece boyunca trafik olmadan da düzenli artıyorsa sızıntı kesindir. Kaynağı bulmak için o sürecin descriptor'larını türüne göre gruplayın:
# Descriptor tiplerini say: REG (dosya), IPv4/IPv6 (soket), sock, FIFO, a_inode
sudo lsof -n -p $PID | awk '{print $5}' | sort | uniq -c | sort -rn
# Aynı dosyanın yüzlerce kez açık olması klasik sızıntı imzasıdır
sudo lsof -n -p $PID | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
En sık gördüğüm üç imza şudur:
- Aynı log dosyası yüzlerce kez açık. Uygulama her istekte dosyayı açıp kapatmayı unutuyordur. Burada asıl bakılacak yer uygulama kodudur; servisin ürettiği kayıtları incelemek için journalctl ile log yönetimi yazısındaki filtreleme yöntemleri işinizi kolaylaştırır.
- Yığınla
CLOSE_WAITdurumunda soket. Karşı taraf bağlantıyı kapatmış, uygulamaclose()çağırmamıştır. Soket durumlarını saymak için açık portları listeleme yazısındakisskullanımından yararlanabilirsiniz:
ss -tan state close-wait | wc -l
(deleted)etiketli dosyalar. Diskten silinmiş ama süreç hâlâ tutuyordur; hem descriptor hem disk alanı sızdırır:
sudo lsof -n +L1 | head -20
Sızıntı tespit ettiğinizde limiti büyütmek doğru hamle değildir; ilgili servisi zamanlanmış bir yeniden başlatmayla ayakta tutup asıl düzeltmeyi uygulama tarafında yapmak gerekir. PHP-FPM'de pm.max_requests değerini sonlu bir sayıya (örneğin 500) çekmek, worker'ları periyodik olarak tazeleyerek sızıntının etkisini sınırlar — kalıcı çözüm değil, ama nefes aldırır.
Kaç Descriptor'a İhtiyacınız Var: Kaba Hesap#
Rastgele büyük bir sayı yazmak yerine ihtiyacınızı kabaca hesaplayabilirsiniz. Nginx için formül şudur:
gereken_descriptor ≈ (eş_zamanlı_bağlantı × 2) + açık_log_dosyası + upstream_soket_havuzu + emniyet_payı
Örnek bir senaryo: saniyede 400 istek alan, ortalama yanıt süresi 200 ms olan bir sitede aynı anda ayakta duran istek sayısı yaklaşık 80'dir. Ancak keepalive bağlantıları çok daha uzun yaşar; 60 saniyelik keepalive_timeout ile aynı anda açık kalan bağlantı birkaç bine çıkabilir. Bu bağlantıların her biri istemci tarafında bir, PHP-FPM tarafında bir descriptor tutar. 4.000 açık bağlantı, tek başına 8.000 descriptor demektir.
PHP-FPM'de hesap daha basittir: her worker aynı anda bir isteği işler, ama o istek sırasında veritabanı bağlantısı, oturum dosyası, açtığı şablon dosyaları ve varsa Redis/Memcached soketi ayrı ayrı descriptor tutar. Worker başına 20–50 descriptor gerçekçi bir tahmindir; 50 worker'lık bir havuz 2.500 civarına ulaşabilir.
Bu sayıları toplayıp iki katını almak, güvenli bir LimitNOFILE değeri verir. Pratikte 65535 orta ölçekli sunucuların neredeyse tamamını rahatlıkla karşılar; bu değerin üzerine ancak on binlerce eş zamanlı bağlantı taşıyan reverse proxy'lerde çıkmak gerekir. Ne kadar bellek harcadığını merak ediyorsanız cevap "çok az": descriptor tablosu ihtiyaç oldukça büyüyen bir yapıdır, tavanı yükseltmek tek başına RAM tüketmez.
Doğrulama Kontrol Listesi ve Sık Yapılan Hatalar#
Değişiklikten sonra "düzeldi" demeden önce şu dört çıktının hepsini görmüş olmalısınız:
# 1) Sistem tavanı ve mevcut kullanım
cat /proc/sys/fs/file-nr
# 2) Master süreçlerin gerçek limiti (kabuğunkine değil, buna bakın)
cat /proc/$(pgrep -o nginx)/limits | grep -i "open files"
cat /proc/$(pgrep -o php-fpm)/limits | grep -i "open files"
# 3) Worker süreçlerinin limiti (master'dan miras almış olmalı)
for p in $(pgrep -f "nginx: worker"); do grep "Max open files" /proc/$p/limits; done
# 4) Hatanın gerçekten kesildiği
sudo grep -c "Too many open files" /var/log/nginx/error.log
Sık yapılan hatalar:
| Hata | Neden işe yaramaz | Doğrusu |
|---|---|---|
limits.conf düzenlemek | systemd servisleri PAM'e uğramaz | systemctl edit SERVISADI ile LimitNOFILE |
systemctl reload nginx | Limit süreç doğarken verilir | systemctl restart nginx |
Yalnızca fs.file-max büyütmek | Hata errno 24, yani süreç tavanı | Katman 2 ve 3 |
| Unit dosyasını doğrudan düzenlemek | Paket güncellemesi üzerine yazar | Drop-in override.conf |
worker_rlimit_nofile yazıp daemon-reload unutmak | systemd override okunmaz | Önce daemon-reload, sonra restart |
| Limiti sürekli büyütmek | Sızıntıyı gizler | Önce sızıntı testi |
Son bir not: paylaşımlı hosting kullanıyorsanız bu katmanların hiçbirine erişiminiz yoktur. Orada aynı belirti farklı bir kılıkta, hesap bazlı kaynak sınırı olarak karşınıza çıkar; o senaryoyu 508 Resource Limit Is Reached hatası yazısında ayrıca ele aldım.
Sıkça Sorulan Sorular#
ulimit -n 65535 yazdım, neden hâlâ hata alıyorum?#
Çünkü o komut yalnızca yazdığınız kabuk oturumunu ve o oturumdan başlattığınız süreçleri etkiler. Nginx, PHP-FPM veya MySQL systemd tarafından başlatıldığı için kabuğunuzun limitinden tamamen bağımsızdır. Gerçek değeri cat /proc/PID/limits ile okuyun; hâlâ 1024 görüyorsanız çözüm systemctl edit SERVISADI ile LimitNOFILE tanımlamaktır.
LimitNOFILE değerini kaç yapmalıyım?#
Eş zamanlı bağlantı hedefinizin en az iki katı, üstüne emniyet payı ekleyerek. Orta ölçekli bir web sunucusu için 65535 fazlasıyla yeterlidir ve ölçülebilir bir maliyeti yoktur; descriptor tablosu ihtiyaç oldukça büyür. Gereksiz yere 1048576 gibi değerler yazmak fayda getirmez, üstelik fs.nr_open tavanını aşarsanız servis hiç başlamaz.
limits.conf tamamen gereksiz mi?#
Hayır, ama kapsamı dardır. SSH ile giriş yapan kullanıcılar, cron dışındaki interaktif oturumlar ve su ile geçilen hesaplar için hâlâ geçerlidir. Elle başlattığınız uzun süreli işlemler, yedekleme scriptleri veya kabuktan çalıştırdığınız derleme işleri bu dosyadan etkilenir. Servisler için ise systemd LimitNOFILE tek geçerli yoldur.
Descriptor sızıntısı olduğunu nasıl kesin anlarım?#
Trafiği düşürün ya da gecenin en sakin saatinde ölçün. Açık descriptor sayısı yük düştüğünde geri iniyorsa limit yetersizliği, inmiyor ve düzenli artmaya devam ediyorsa sızıntı vardır. lsof -n -p PID çıktısında aynı dosya adının onlarca kez tekrarlanması veya ss -tan state close-wait sayısının sürekli büyümesi bunu doğrular.
Nginx'te worker_rlimit_nofile ile worker_connections arasındaki ilişki nedir?#
Her bağlantı istemci tarafında bir, upstream tarafında bir descriptor tüketir. Bu yüzden bir worker'ın kaldırabileceği gerçek bağlantı sayısı worker_rlimit_nofile / 2 civarındadır. worker_connections değerini bu sayının üzerine çıkarırsanız Nginx bağlantıyı kabul eder ama descriptor bulamayıp hata verir. Pratik kural: worker_rlimit_nofile en az worker_connections × 2 olsun.
Ayarı yaptım, reload ettim, neden değişmedi?#
RLIMIT_NOFILE bir sürece doğduğu anda verilir ve çalışırken kendiliğinden güncellenmez. reload, master süreci yerinde bırakıp yalnızca yapılandırmayı tazeler; eski limit aynen devam eder. systemctl daemon-reload ile systemd'nin override'ı okumasını sağlayın, ardından systemctl restart ile süreci gerçekten yeniden doğurun. Kesinti kabul edilemiyorsa geçici olarak prlimit kullanabilirsiniz.