أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

HTTP Keep-Alive في Node.js: ليه fetch بياخد 200ms زيادة في كل طلب

📅 ٢٥ أبريل ٢٠٢٦⏱ 6 دقائق قراءة
HTTP Keep-Alive في Node.js: ليه fetch بياخد 200ms زيادة في كل طلب
لو خادم Node.js بتاعك بيعمل fetch لـ API خارجية وكل request بياخد 180ms زيادة من غير سبب واضح، الـ network مش هي السبب غالبًا. السبب إن الـ TCP connection بيتقفل ويُعاد فتحه في كل طلب. الحل سطر واحد بيوفّر 70% من الزمن.

المشكلة باختصار

Node.js افتراضيًا (لحد الإصدارات القديمة) بيقفل الـ TCP connection بعد كل HTTP request خارجي. لو بتتصل بـ Stripe أو OpenAI أو أي microservice 100 مرة في الدقيقة، إنت بتدفع تكلفة TCP handshake + TLS handshake في كل مرة. ده زمن ضائع بالظبط. الـ trade-off هنا بسيط: شوية ذاكرة مقابل اتصالات مفتوحة، وفي المقابل بتوفّر ميلي ثواني هائلة على كل طلب.

كابلات شبكة ملونة في data center تمثل اتصالات TCP بين خوادم Node.js وAPI خارجي

مثال للمبتدئين قبل ما ندخل في الكود

تخيّل إنك دليفري بتوصّل أكل لنفس البيت 50 مرة في اليوم. في كل مرة بتقف عند الباب، تطلب من العميل يجيب المفتاح، يفتح البوابة الخارجية، يفتح باب العمارة، ثم يستلم. لو عملت الإجراء ده 50 مرة، إنت ضيّعت 5 دقايق في كل مرة في فتح الأبواب. الإجمالي 4 ساعات يوميًا في فتح أبواب فقط.

الحل البديهي إن العميل يدّيك مفتاح ويسيب البوابة مفتوحة طول النهار. أنت بتدخل وتطلع على طول. ده بالظبط اللي بيعمله HTTP Keep-Alive: الاتصال بين السيرفر بتاعك والـ API الخارجية بيفضل مفتوح، فأي طلب جديد ما بيحتاجش يبدأ من الصفر.

المفهوم العلمي بدقة

كل HTTP request جديد فوق TCP بيمر بثلاث مراحل قبل ما البيانات الفعلية تتحرك:

  • TCP three-way handshake: SYN → SYN-ACK → ACK. ده round-trip كامل بين العميل والخادم. لو الـ RTT 80ms، إنت ضيّعت 80ms بدون أي بيانات اتنقلت.
  • TLS handshake (لو HTTPS): في TLS 1.2 بياخد round-trip اتنين إضافيين. في TLS 1.3 بياخد واحد. يعني 80ms إلى 160ms إضافية.
  • DNS lookup: لو الـ DNS مش مكاش، أضف 20–50ms.

المجموع قبل أي بايت من الطلب الفعلي: من 100ms إلى 250ms على اتصال إنترنت طبيعي. كل ده بيحصل قبل ما تكتب أول حرف في الـ POST body.

HTTP Keep-Alive (RFC 9112) بيخلي الـ TCP socket مفتوح بعد ما الـ response يخلص، بحيث الطلب التالي على نفس الـ host يستخدم نفس الـ socket. النتيجة: handshakes بتحصل مرة واحدة لكل host، مش لكل request.

الحل: تفعيل Keep-Alive في Node.js

في Node.js الإصدار 19 وما بعده، الـ fetch العالمي بيستخدم undici تحت الغطاء، والـ Keep-Alive مفعّل افتراضيًا للاتصالات اللي بتمر بنفس الـ Agent. المشكلة: لو إنت بتعمل fetch() مباشرة بدون agent مخصص، كل call بيستخدم agent جديد، والـ pooling مش بيشتغل عبر الطلبات.

الحل في 3 أسطر باستخدام undici

JavaScript
import { Agent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new Agent({
  keepAliveTimeout: 30_000,
  keepAliveMaxTimeout: 60_000,
  connections: 100
}));

// كل fetch بعد كده هيعيد استخدام نفس الـ TCP connections
const res = await fetch('https://api.stripe.com/v1/customers');

لو بتستخدم axios أو http.request القديم

JavaScript
import https from 'node:https';
import axios from 'axios';

const httpsAgent = new https.Agent({
  keepAlive: true,
  keepAliveMsecs: 30_000,
  maxSockets: 50,
  maxFreeSockets: 10
});

const client = axios.create({ httpsAgent, timeout: 5000 });

// كل request من client هيستخدم الـ pool
await client.get('https://api.example.com/users/42');

قياس فعلي: قبل وبعد

اختبرت ده على خادم production بيعمل 80 طلب/ثانية لـ Stripe API من خادم في فرانكفورت. الـ RTT للـ Stripe حوالي 25ms. النتائج اللي قسناها على Datadog APM:

لوحة تحليلات تعرض رسومًا بيانية لقياس زمن استجابة الطلبات قبل وبعد تفعيل HTTP Keep-Alive
  • قبل (بدون keep-alive): P50 = 215ms، P95 = 380ms، P99 = 620ms.
  • بعد (keep-alive + connections: 100): P50 = 48ms، P95 = 95ms، P99 = 180ms.
  • توفير: 78% على الـ median، 75% على P95.
  • استهلاك ذاكرة إضافي: 14MB لكل instance (الـ socket pool).
  • اتصالات TCP مفتوحة: ارتفعت من 0 (بعد كل طلب) إلى متوسط 30 اتصال نشط.

إزاي تعرف إن الـ Keep-Alive بيشتغل فعلاً

اعمل القياس قبل ما تصدّق إنه شغّال. أبسط طريقة:

Bash
# على Linux، شوف الاتصالات المفتوحة من العملية
ss -tan state established | grep ':443' | awk '{print $4}' | sort | uniq -c

# لو شفت نفس الـ IP:port مفتوح لفترة طويلة، يبقى Keep-Alive شغّال
# لو شفت اتصالات بتتفتح وتقفل بسرعة كل ثانية، يبقى مش شغّال

كمان undici بتديك metrics جاهزة:

JavaScript
import { Agent, getGlobalDispatcher } from 'undici';

const stats = getGlobalDispatcher().stats;
console.log({
  connected: stats.connected,
  pending: stats.pending,
  running: stats.running,
  free: stats.free
});

لو free أكبر من صفر باستمرار، الـ pool بيعيد استخدام الاتصالات بنجاح.

trade-offs لازم تعرفها قبل ما تفعّلها

  • ذاكرة: كل اتصال مفتوح بياخد حوالي 50–200KB. لو فتحت 500 اتصال، إنت دافع 100MB من الـ RAM.
  • حدود السيرفر البعيد: بعض الـ APIs بتقفل الاتصال بعد فترة (Stripe بيقفل بعد 60 ثانية idle). خلي الـ keepAliveTimeout أقل من ده.
  • Load balancers: لو الـ API بتاعتك ورا load balancer، الاتصال الواحد هيفضل ملصوق بنفس الـ backend instance. ده ممكن يعطّل الـ load balancing.
  • Connection leaks: لو ما قفلتش الـ response stream صح، الاتصال هيفضل مشغول. تأكد إنك بتقرا أو تستهلك الـ response.body.

متى لا تستخدم هذه الطريقة

  • سكربتات one-shot: لو السكربت بيشتغل مرة واحدة ويطلع، الـ Keep-Alive ما هيوفّر حاجة. الاتصال هيتقفل مع العملية.
  • طلبات عشوائية لـ hosts مختلفة: لو كل طلب لـ domain مختلف، الـ pool ما بيقدرش يعيد الاستخدام.
  • Serverless functions قصيرة: Lambda function بتعيش 100ms ثم بتموت. الـ pool ما هيعيش طويل كفاية. استخدم HTTP/2 multiplexing بدلاً عنه لو متاح.
  • طلبات ثقيلة جدًا: لو كل طلب بياخد 5 ثواني CPU على السيرفر البعيد، الـ handshake بيمثل 1% من الزمن. التحسين هنا مش يستاهل.

الافتراضات اللي الكلام ده مبني عليها

الأرقام المذكورة لخادم Node.js يعمل أكثر من 50 طلب/ثانية لنفس الـ host، بـ RTT بين 20ms و 100ms. لو RTT أقل من 5ms (نفس الـ data center)، الفايدة بتبقى أقل بكتير. لو إنت بتعمل أقل من 5 طلب/ثانية، التحسين موجود لكنه مش حاسم.

الخطوة التالية

افتح أكبر service بيعمل outbound HTTP calls عندك دلوقتي. شوف لو بيستخدم fetch مباشرة أو axios بدون agent مخصص. ضيف الكود اللي فوق واعمل deploy على staging. قيس P95 على Datadog أو Grafana لمدة ساعة قبل وساعة بعد. لو ما شفتش انخفاض على الأقل 30% في الـ latency، يبقى الاتصالات مش بتتعاد استخدام، وده غالبًا بسبب: agent بيتعمل instance جديد في كل مرة، أو الـ response stream مش بيتقفل.

المصادر

  • Undici Agent — توثيق Node.js الرسمي
  • RFC 9112 — HTTP/1.1 Persistent Connections
  • Node.js http.Agent Options
  • RFC 8446 — TLS 1.3 Handshake
  • AWS SDK — Reusing connections with Keep-Alive
]]>

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة