الـ Retries و Backoff: ماذا تفعل عندما يفشل الـ API مؤقتاً؟
عندما تبني تكاملاً يعتمد على API خارجي، فأنت لا تتعامل مع منطقك البرمجي فقط، بل مع شبكة، وخوادم، وسياسات حماية، وازدحام لحظي، وأحياناً تحديثات مؤقتة لا تعلم عنها شيئاً. هنا يظهر مفهوم Retries وBackoff كأحد أهم أساليب جعل الأتمتة أكثر استقراراً وواقعية.
الفكرة ببساطة: ليس كل فشل يعني أن الطلب مات نهائياً. كثير من الإخفاقات تكون مؤقتة، مثل انقطاع قصير في الاتصال، أو بطء في الخادم، أو رد 503 بسبب حمل مرتفع، أو حتى رد 429 لأنك تجاوزت الحد المسموح للطلبات. إذا كنت قد قرأت رموز الحالة (HTTP Status Codes): ماذا يخبرك السيرفر بـ 200 أو 404؟ فستعرف أن الكود نفسه غالباً يخبرك إن كان الفشل دائماً أم مؤقتاً.
في أنظمة الأتمتة الحقيقية، وخصوصاً عند ربط منصات تعليمية، وأدوات بريد، وأنظمة محاسبة، ومزودي ذكاء اصطناعي، فإن تجاهل إعادة المحاولة يجعل السيناريو هشاً. بينما تنفيذ إعادة المحاولة بشكل عشوائي قد يحول سكربتك إلى مصدر إزعاج للخادم. لذلك المطلوب ليس فقط إعادة الإرسال، بل إعادة ذكية ومدروسة.
ما المقصود بـ Retries وBackoff؟
Retries تعني أن النظام يعيد إرسال نفس الطلب بعد فشله، بدلاً من إعلان الانهيار من أول محاولة. أما Backoff فيعني أن الانتظار بين المحاولات لا يكون ثابتاً دائماً، بل يزداد وفق نمط معين لتقليل الضغط على الخادم ورفع احتمال نجاح المحاولة التالية.
هذا المفهوم مرتبط مباشرة بأساسيات تصميم الطلبات التي تناولناها في تشريح طلب الـ API: الـ Endpoint، الـ Headers، والـ Body، لأنك لا تعيد الإرسال بشكل أعمى، بل تعيد نفس Request مع فهم كامل لطبيعته، وهل هو آمن للإعادة أم لا.
مثال عملي مبسط
- المحاولة الأولى تفشل بسبب مهلة اتصال
Timeout. - ينتظر السكربت ثانيتين.
- يعيد الطلب مرة ثانية.
- إذا فشل مجدداً، ينتظر أربع ثوانٍ.
- ثم يعيد المحاولة الثالثة قبل إعلان الخطأ النهائي.
هذه الآلية البسيطة تمنع كثيراً من الأعطال الوهمية التي تبدو للمستخدم أخطاء كبيرة، بينما هي في الحقيقة اضطرابات عابرة في الشبكة أو البنية التحتية.
متى يكون الفشل مؤقتاً ومتى لا يجب إعادة المحاولة؟
هنا يقع الخطأ الشائع. بعض المطورين يعيدون المحاولة مع كل خطأ، وهذا سلوك سيئ تقنياً وقد يؤدي إلى حظر حسابك أو استنزاف الحصة اليومية. القاعدة الذهبية: أعد المحاولة فقط عندما يكون هناك احتمال منطقي أن الطلب قد ينجح لاحقاً.
أمثلة على الأخطاء القابلة لإعادة المحاولة:
408مهلة طلب،429تجاوز المعدل،500خطأ داخلي،502بوابة معطلة،503خدمة غير متاحة،504مهلة بوابة، وأخطاء الشبكة مثل انقطاعDNSأو فشلTCP.أمثلة على الأخطاء غير المناسبة لإعادة المحاولة:
400لأن البيانات خاطئة،401لأن التوثيق فشل،403لأن الوصول ممنوع، و404إذا كانEndpointأو المورد غير موجود فعلاً.
إذا كان السبب هو انتهاء صلاحية الرمز، فالحل ليس Retry فوري، بل تجديد الاعتماديات أولاً كما شرحنا في التعامل مع الـ Bearer Tokens وتجديد الصلاحيات آلياً.
لماذا لا يكفي الانتظار الثابت؟
الانتظار الثابت مثل 3 ثوانٍ بين كل محاولة قد يعمل في حالات بسيطة، لكنه ليس أفضل اختيار في الأنظمة المزدحمة. إذا فشلت مئات العمليات في نفس اللحظة ثم أعادت المحاولة بعد نفس المدة تماماً، فإنها تصنع موجة ضغط جديدة على الخادم. هذه المشكلة تعرف أحياناً باسم Retry Storm.
لهذا نستخدم غالباً Exponential Backoff، أي أن زمن الانتظار يتضاعف تدريجياً: ثانية، ثم ثانيتان، ثم أربع، ثم ثمانٍ. ويمكن تحسينه أكثر بإضافة Jitter، أي مقدار عشوائي صغير يمنع كل العملاء من إعادة الإرسال في نفس اللحظة.
الأنماط الأكثر استخداماً
Fixed Backoff: تأخير ثابت بين المحاولات.Linear Backoff: زيادة تدريجية بسيطة مثل 2 ثم 4 ثم 6 ثوانٍ.Exponential Backoff: تضاعف متدرج وهو الأكثر شيوعاً.Exponential Backoff with Jitter: الأفضل غالباً في البيئات الإنتاجية.
كيف تبني منطق إعادة المحاولة داخل سكربت الأتمتة؟
قبل كتابة الكود، حدد سياسة واضحة: كم محاولة مسموحة؟ ما الأخطاء القابلة لإعادة المحاولة؟ هل الطلب من النوع Idempotent أم قد يؤدي تكراره إلى إنشاء بيانات مكررة؟ هذا مهم جداً خصوصاً مع طلبات POST التي قد تنشئ سجلات جديدة إذا أُرسلت مرتين.
إذا أردت فهماً أعمق لأنواع الطلبات، فارجع إلى شرح أفعال الـ HTTP (GET, POST, PUT, DELETE) والفرق بينها. كما أن موضوع تحديد معدل الطلبات (Rate Limiting): كيف تتجنب الحظر من الخوادم مكمل أساسي هنا، لأن Backoff ليس مجرد تحسين، بل أداة امتثال لسياسات المزود.
async function fetchWithRetry(url, options = {}, maxRetries = 4, baseDelay = 1000) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
const shouldRetry = retryableStatusCodes.includes(response.status);
if (!shouldRetry || attempt === maxRetries) {
throw new Error(`Request failed with status ${response.status}`);
}
const retryAfter = response.headers.get("Retry-After");
let delay = retryAfter
? Number(retryAfter) * 1000
: baseDelay * Math.pow(2, attempt);
const jitter = Math.floor(Math.random() * 300);
delay += jitter;
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
const isLastAttempt = attempt === maxRetries;
if (isLastAttempt) {
throw error;
}
const delay = baseDelay * Math.pow(2, attempt) + Math.floor(Math.random() * 300);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
هذا المثال يطبق ثلاث ممارسات ممتازة: التحقق من Status Code، احترام رأس Retry-After إن وُجد، وإضافة Jitter لتخفيف التزامن الجماعي.
توثيق سياسة الفشل داخل فريقك أو منتجك
كثير من مشاكل الأتمتة لا تكون في الكود نفسه، بل في غياب التوثيق. عندما يعمل فريق على سيناريوهات متعددة بين REST API وWebhook وعمليات مجدولة عبر CRON، يجب أن يعرف الجميع متى تتم إعادة المحاولة، ومتى يُرسل تنبيه، ومتى تتوقف العملية نهائياً.
سياسة مقترحة لتوثيق
Endpointحرج:– الحد الأقصى للمحاولات:
4
– نمط التأخير:Exponential Backoff with Jitter
– الحالات المسموحة للإعادة:408, 429, 500, 502, 503, 504
– التعامل مع401: تنفيذ تجديد رمز ثم إعادة طلب واحدة فقط
– التعامل مع الفشل النهائي: تسجيل مفصل في السجلات وإرسال تنبيه إلى البريد أوTelegram
هذا النوع من التوثيق ينسجم مع ما شرحناه سابقاً في توثيق الـ API: كيفية قراءة مستندات Swagger و Redoc، ويمنع القرارات الارتجالية وقت الأعطال.
أفضل الممارسات في بيئات الأتمتة الحقيقية
- لا تعِد محاولة كل شيء؛ اربط القرار بنوع الخطأ وسياق العملية.
- استخدم معرفات فريدة أو مفاتيح
Idempotencyعندما يكون التكرار خطيراً. - احترم رؤوس الاستجابة مثل
Retry-Afterبدلاً من فرض توقيتك الخاص دائماً. - سجّل رقم المحاولة والسبب والزمن المستغرق حتى تستطيع التحليل لاحقاً.
- أرسل تنبيهاً بعد الفشل النهائي، ولا تكتفِ بالصمت داخل السجلات.
- ادمج منطق
Retriesمع منهجية معالجة الأخطاء (Error Handling): كيف تجعل نظامك “مضاداً للكسر” بدل اعتباره حلاً منفصلاً.
الخلاصة
الفشل المؤقت في عالم API Integration ليس استثناءً، بل جزء طبيعي من التشغيل اليومي. الفرق بين سكربت هاوٍ ونظام إنتاجي محترم هو أن الثاني يتوقع هذا الفشل، ويفهمه، ويستجيب له بسياسة محسوبة لا تضر بالخادم ولا بالمستخدم.
لذلك، إذا كنت تبني أتمتة تعتمد على خدمات خارجية، فتعامل مع Retries وBackoff كجزء من التصميم الأساسي، لا كترقيع لاحق. عندها فقط ستتحول عملياتك من مجرد استدعاءات متفرقة إلى بنية أتمتة resilient يمكن الوثوق بها تحت الضغط، وعند الانقطاعات، ومع نمو حجم البيانات والطلبات.