حلّ خطأ «المنفذ قيد الاستخدام» (EADDRINUSE) على macOS
تعطّل خادم التطوير، فأعدت تشغيله، فاستقبلك بـError: listen EADDRINUSE: address already in use :::3000. الخبر السار: المشكلة شكلية تقريباً دائماً. هنا سبب حدوثها، والطريقة الآمنة لتحرير المنفذ، وأمثلة عملية على البيئات الأكثر معاناةً منها.
ما يعنيه EADDRINUSE فعلاً
EADDRINUSE هو الخطأ الذي تُعيده النواة حين تستدعي عملية bind() على زوج عنوان+منفذ TCP/UDP محجوز سلفاً. ليس خطأً في منطق تطبيقك أبداً — بل النظام يرفض حجز المنفذ مرتين. سيناريوهان يُنتجان هذا الخطأ:
- عملية أخرى تستمع فعلاً على المنفذ.
- النسخة السابقة من عمليتك خرجت، لكن النواة لا تزال تعتبر المقبس مستخدماً (عادةً
TIME_WAIT).
الحلّ مختلف في كل حالة.
الخطوة 1: عرف من يحتجز المنفذ
أسرع إجابة عبر lsof:
lsof -nP -iTCP:3000 -sTCP:LISTEN
ثلاث نتائج محتملة:
- صف واحد، باسم عمليتك (
nodeأوrubyأوpython…). تشغيل سابق لا يزال حياً. اذهب للخطوة 2. - صف واحد، عملية غريبة. شيء آخر يحتلّ المنفذ. إمّا غيّر المنفذ أو أوقِف تلك العملية.
- لا مخرجات. لا شيء يستمع — لكنك ما زلت تتلقّى
EADDRINUSE. هذه هيTIME_WAIT. اذهب للخطوة 3.
(للاطّلاع الأشمل على lsof، راجع إيجاد العملية المستخدمة لمنفذ على macOS.)
الخطوة 2: تحرير المنفذ — بهدوء
إن كان المتسبّب عمليتك أنت، أنهِها أوّلاً بـSIGTERM:
kill $(lsof -ti :3000)
SIGTERM هي الإشارة المهذّبة. تلتقطها العملية، تُغلق مقابسها، تُفرغ أيّ كتابات معلّقة، تحذف ملف الـPID، ثم تخرج. هذا ما تريد.
إن أعادت lsof -ti :3000 الـPID نفسه بعد ثوانٍ، صعِّد:
kill -9 $(lsof -ti :3000)
SIGKILL لا تُقاوَم — العملية لا تراها أصلاً. لذلك هي الملاذ الأخير: أيّ شيء كان يُفترض أن تنظّفه (ملفات قفل، مجلّدات مؤقّتة، مقابس في CLOSE_WAIT) سيبقى متروكاً.
لا تَلجأ تلقائياً إلىsudo kill -9. إن لم يستطع shell-ك إنهاء العملية، فالخطوة الصحيحة معرفة السبب — غالباً لأنها تعمل بمستخدم آخر. إضافةsudoتُفاقم مشكلة التنظيف.
الخطوة 3: لا شيء يستمع لكن المنفذ مشغول
تظهر EADDRINUSE، لكن lsof لا يُظهر شيئاً. هذه TIME_WAIT: مصافحة إغلاق TCP تُبقي المقبس متلكّئاً 30–120 ثانية لتُطابق الحزم المتأخّرة وتُسقطها بدلاً من تسليمها لخادم جديد على المنفذ نفسه.
أمامك ثلاثة خيارات:
- انتظر قليلاً. دقيقتان كحدّ أقصى، أقل غالباً.
- اطلب من خادمك تفعيل
SO_REUSEADDR. يسمح ذلك للنواة بإعادة استخدام مقبسTIME_WAIT. معظم الأُطر تفعّله افتراضياً. إن لم يفعل، أَضِفه إلى استدعاء الـbind. - اربط بمنفذ آخر. غالباً أسهل مسار في التطوير.
يمكنك التأكّد من الحالة بـnetstat:
netstat -anv -p tcp | grep 3000
أسطر تنتهي بـTIME_WAIT هي السبب.
أمثلة عملية
Node / Express / Next.js / Vite
Error: listen EADDRINUSE: address already in use :::3000
السبب: nodemon أو npm run dev أو تبويب آخر في طرفية أخرى لا يزال يحتجز المنفذ. الحلّ:
lsof -ti :3000 | xargs kill
في Next.js أو Vite، يمكنك أيضاً تمرير منفذ مختلف: PORT=3001 npm run dev أو vite --port 5174.
Rails / Puma
A server is already running. Check /tmp/pids/server.pid
يرفض Rails التشغيل إن كان ملف الـPID موجوداً. حلّان:
# 1. العملية ميتة، فقط ملف PID قديم
rm tmp/pids/server.pid
# 2. العملية حيّة، أنهِها أوّلاً
kill $(cat tmp/pids/server.pid)
rm tmp/pids/server.pid
Python / Flask / Django
OSError: [Errno 48] Address already in use
النمط نفسه: lsof -ti :8000 | xargs kill. خادم Django التطويري يفعّل SO_REUSEADDR عادةً، لذا هذه عملية متلكّئة حقيقية في الغالب — لا TIME_WAIT.
Docker
Error: Bind for 0.0.0.0:5432 failed: port is already allocated
نَكهتان:
- حاوية سابقة لا تزال تحتجزه.
docker ps، ثمdocker stop <name>. - عملية على المضيف تحتجزه. تثبيت Postgres الأصلي يربط
5432غالباً.lsof -i :5432سيخبرك. إمّا أوقف خدمة المضيف (brew services stop postgresql) أو أعِد ربط منفذ المضيف للحاوية (5433:5432في docker-compose).
الوقاية: ترك أشباح أقل
- استخدم مدير عمليات في التطوير (foreman أو overmind أو pm2). يوقفون أبناءهم بثبات عند Ctrl+C.
- فعّل
SO_REUSEADDRفي كود المقابس المخصّص. Express و Rails و Django و FastAPI يفعلون ذلك نيابة عنك؛ الخوادم اليدويّة لا تفعل غالباً. - التقط الإشارات في الإنتاج. الخادم الذي يُعالج SIGTERM لا يترك مخلّفات
TIME_WAIT— يُغلق بنظام. - راقب شريط القوائم. العرض الحيّ للمنافذ المستمعة يُلغي تساؤل «أَلَم أكن قد أنهيتُها؟».