تخطّى إلى المحتوى الرئيسي
المهام الخلفية على Vercel في 2026: كيف نختار بين waitUntil وQueues وWorkflow وCron

المهام الخلفية على Vercel في 2026: كيف نختار بين waitUntil وQueues وWorkflow وCron

August 7, 2026
هندسة الخوادم
7 دقيقة قراءة

لم تقضِ البنية بدون خوادم على المهام الخلفية، بل قضت على المهام التي تستمر بعد إرسال الاستجابة دون إبلاغ بيئة التشغيل. في Devya نوجّه كل مهمة مؤجَّلة على Vercel عبر واحدة من أربع أدوات: ‏waitUntil()‎ للآثار الجانبية القصيرة التي يُقبل فقدانها، وVercel Cron للمهام المجدولة زمنياً، وVercel Queues للعمل الذي يجب أن ينجو من فشل المستهلك، وVercel Workflow للمهام متعددة الخطوات التي يجب أن تنجو من إعادة نشر بعد ساعة.

أهم النقاط

الدالة waitUntil()‎ من حزمة ‎@vercel/functions تُبقي دالة Vercel حية بعد إرسال الاستجابة حتى ينتهي الوعد المعلّق، لكنها بأفضل جهد فقط: لا إعادة محاولة، ولا استمرارية، وتموت مع انتهاء الاستدعاء.

‏Vercel Queues خدمة بث أحداث دائمة بتسليم «مرة واحدة على الأقل»، ما يعني أن كل مستهلك يجب أن يكون idempotent لأن بعض الرسائل ستُسلَّم مرتين حتماً.

‏Vercel Workflow يوفّر تنفيذاً دائماً: الدالة غير المتزامنة المُعلَّمة بتوجيه "use workflow" تحفظ نقطة تفتيش لكل خطوة، فيستأنف الانهيار من آخر خطوة مكتملة بدل البدء من الصفر.

‏Vercel Cron مناسب فقط للعمل المرتبط بالوقت. أي مهمة يطلقها إجراء من المستخدم مكانها الطابور، لا الجدول الزمني.

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

لماذا يختفي العمل الخلفي بعد إرجاع الاستجابة؟

يختفي العمل الخلفي لأن الوعد غير المنتظَر لا مالك له. عندما تُرجع دالة Vercel كائن Response، تصبح المنصة حرة في تجميد النسخة أو استرجاعها فوراً، وأي وعد ما زال قيد التنفيذ يُلغى عند نقطة عشوائية.

أسوأ نمط فشل واجهناه ليس أن العمل لا يعمل أبداً، بل أنه يعمل أحياناً. فـFluid Compute، وهو نموذج الحوسبة الافتراضي على Vercel، يعيد استخدام نسخة الدالة نفسها عبر طلبات متزامنة بدل إنشاء نسخة لكل طلب. لذلك يكتمل الوعد المعلّق غالباً لأن طلباً آخر أبقى النسخة دافئة، ويختفي بصمت عند انخفاض حركة المرور. والفقد غير الحتمي أصعب بكثير في الاكتشاف من الفقد الكامل.

متى تكفي waitUntil()؟

تكفي waitUntil()‎ حين يكون فقدان العمل أحياناً مقبولاً وينتهي العمل خلال ثوانٍ معدودة. الدالة waitUntil(promise)‎ مُصدَّرة من ‎@vercel/functions وتسجّل الوعد لدى بيئة التشغيل، فيبقى الاستدعاء حياً حتى يُحسم الوعد رغم أن الاستجابة أُرسلت بالفعل.

هناك حدّان يجب ذكرهما بصراحة. أولاً، لا تملك waitUntil()‎ أي دلالات لإعادة المحاولة: إذا رُفض الوعد فلن يعيد أحد تشغيله، ويظهر الرفض في سجلات التشغيل فقط. ثانياً، يظل العمل المؤجَّل محسوباً ضمن المهلة القصوى للدالة وضمن فوترة Active CPU، لأن waitUntil()‎ تؤجّل العمل بالنسبة للاستجابة لا بالنسبة للاستدعاء. نستخدمها لأحداث التحليلات وتسخين الكاش وشحن السجلات، ولا نستخدمها أبداً لأي عمل قد يفتح المستخدم بسببه تذكرة دعم.

ما الذي تحلّه Vercel Queues فعلياً؟

تحلّ Vercel Queues الحالة التي يجب فيها أن يحدث العمل في النهاية حتى لو فشلت المحاولة الأولى. وVercel Queues نظام بث أحداث دائم مبني على Fluid Compute، وهو حالياً في نسخة تجريبية عامة، ويوفّر تسليماً مرة واحدة على الأقل: يكتب المنتج رسالة إلى موضوع ويعود فوراً، بينما تعالج دالة مستهلك منفصلة تلك الرسالة مع إعادة محاولة عند الفشل.

المكسب المعماري هو فصل ميزانيات زمن الاستجابة. بدون طابور يصبح p95 لمسار الـAPI مساوياً لـp95 لأبطأ طرف ثالث يستدعيه، سواء مزوّد بريد أو مولّد PDF أو توزيع webhooks. ومع الطابور يصبح p95 للمسار هو تكلفة كتابة دائمة واحدة، ويتحوّل يوم الطرف الثالث السيئ إلى منحنى إعادة محاولة على المستهلك بدل انتهاء مهلة على طلب المستخدم.

الضريبة هي الـidempotency، وهي ليست اختيارية. التسليم مرة واحدة على الأقل يعني أن التسليم المكرر يقين على مدى زمني كافٍ، لا حالة حدّية. نمطنا الافتراضي جدول إزالة تكرار بقيد فريد على معرّف الرسالة: نُدخل المعرّف أولاً، ونعتبر التعارض دليلاً على أن العمل تم فنعود، ولا ننفّذ الأثر الجانبي إلا إذا أنشأ الإدخال صفاً فعلياً.

ما تزال واجهة Queues تتغيّر أثناء النسخة التجريبية العامة، لذا تعامل مع النمط السابق كشكل عام لا كتوقيع نهائي، وتحقّق من التوثيق الحالي قبل الربط.

متى نستخدم Vercel Workflow بدل الطابور؟

استخدم Vercel Workflow حين تكون وحدة إعادة المحاولة خطوة واحدة داخل مهمة أطول، لا الرسالة كاملة. وVercel Workflow إطار تنفيذ دائم: تُعلَّم الدالة غير المتزامنة بتوجيه "use workflow" وتُعلَّم خطواتها بتوجيه "use step"، وتحفظ بيئة التشغيل نتيجة كل خطوة مكتملة كنقطة تفتيش، فيستأنف أي انقطاع من آخر نقطة بدل إعادة كل شيء.

هذا التمييز هو القرار كله. رسالة الطابور ذرّية: إذا رمى المعالج خطأً عند السطر 40 يُعاد تسليم الرسالة كاملة وتُنفَّذ الأسطر من 1 إلى 39 مجدداً. هذا مقبول إذا كانت تلك الأسطر نقية، وغير مقبول إذا كان السطر 12 قد خصم من بطاقة بينما فشل السطر 40 في توليد ملف PDF.

‏Workflow يمنحك الوقت أيضاً. يستطيع سير العمل الدائم أن ينام ساعات أو أياماً وينتظر حدثاً خارجياً، لأن حالته تعيش خارج أي استدعاء دالة واحد. أما مستهلك الطابور فلا يستطيع، لأنه ما زال دالة محكومة بمهلة 300 ثانية.

كيف نختار بين Cron وwaitUntil وQueues وWorkflow؟

‏waitUntil()‎ — يُطلَق بطلب، غير دائم، بلا إعادة محاولة. مناسب لأحداث التحليلات وتسخين الكاش وشحن السجلات.

‏Vercel Cron — يُطلَق بالساعة، جدوله دائم، وإعادة المحاولة عملياً هي الدورة التالية. مناسب للمسح الليلي ومهام انتهاء الصلاحية وتوليد التقارير.

‏Vercel Queues — يُطلَق برسالة من منتج، دائم، وإعادة تسليم مرة واحدة على الأقل. مناسب لإرسال البريد وتوزيع الـwebhooks ومعالجة الصور.

‏Vercel Workflow — يُطلَق باستدعاء صريح، دائم على مستوى كل خطوة، ويستأنف من آخر نقطة تفتيش. مناسب لتسلسلات التهيئة وتنسيق عدة مزوّدين والمهام الطويلة.

يُضبط Vercel Cron بشكل تعريفي. في ملف vercel.ts، وهو إعداد المشروع الموصى به بلغة TypeScript والبديل عن vercel.json، يكون مصفوفة crons من أزواج المسار والجدول، مثل المسار ‎/api/cleanup بالجدول ‎0 3 * * *‎.

ما الذي انكسر معنا فعلياً؟

انكسرت ثلاثة أشياء، وجميعها في الفجوة بين نجاح الأداة واحترام المعالج لعقد تلك الأداة.

أثر جانبي مكرر بسبب التسليم مرة واحدة على الأقل. مستهلك يرسل بريد تأكيد لم يكن لديه حارس إزالة تكرار، وتسبب فشل عابر بعد الإرسال وقبل الإقرار في إعادة التسليم، فاستلم المستخدم نفسه البريد نفسه مرتين. الحل كان نمط «أدخل ثم نفّذ»: كتابة معرّف الرسالة تحت قيد فريد أولاً واعتبار التعارض دليل إنجاز سابق.

مهمة طويلة موضوعة داخل waitUntil()‎. أجّلنا مهمة مستندات تستغرق دقائق باستخدام waitUntil()‎ لأنها كانت أصغر تعديل ممكن. عملت في بيئة الاختبار وفقدت عملاً في الإنتاج أثناء النشر، لأن النشر التدريجي يُنهي النسخة القديمة فيذهب معها الوعد الجاري. تلك المهمة كان مكانها الطابور من البداية، وكانت waitUntil()‎ عقداً خاطئاً لا مشكلة ضبط.

تداخل تشغيلات Cron. نما مسح ليلي حتى تجاوز فترته الزمنية فعمل استدعاءان بالتوازي على الصفوف نفسها. ولا يقوم Vercel Cron بجدولة التنفيذات المتداخلة تسلسلياً نيابةً عنك. أضفنا قفل advisory في Postgres في أعلى المعالج ليخرج التشغيل الثاني فوراً بدل التنازع.

النمط خلف الحالات الثلاث: اختر الأداة بحسب الفشل الذي تحتمله، لا بحسب الشيفرة الأسرع كتابةً. العمل بأفضل جهد يأخذ waitUntil()‎، والعمل الذي يجب أن يحدث يأخذ طابوراً ومفتاح idempotency، والعمل الذي يجب أن يحدث بترتيب يأخذ workflow، والعمل الزمني يأخذ cron وقفلاً.

الأسئلة الشائعة

س: هل تسمح waitUntil()‎ لدالة Vercel بالعمل لفترة أطول من مهلتها القصوى؟

ج: لا. تُبقي waitUntil()‎ الاستدعاء حياً بعد إرسال الاستجابة، لكن الاستدعاء يظل محكوماً بالمهلة القصوى للدالة، وهي 300 ثانية افتراضياً على كل الخطط في 2026. فهي تؤجّل العمل بالنسبة للاستجابة لا بالنسبة للاستدعاء.

س: هل ما زلنا نحتاج طابوراً من طرف ثالث مثل SQS أو BullMQ أو Inngest على Vercel؟

ج: ليس في الحالات الشائعة. يغطي Vercel Queues المراسلة الدائمة مرة واحدة على الأقل، ويغطي Vercel Workflow التنفيذ الدائم متعدد الخطوات، وكلاهما أصلي على Fluid Compute. اللجوء لنظام خارجي يكون حين تحتاج دلالات لا يوفّرانها، مثل ترتيب FIFO صارم لكل مفتاح أو معالجة مرة واحدة بالضبط يفرضها الوسيط.

س: ماذا يعني التسليم مرة واحدة على الأقل عملياً في شيفرة المستهلك؟

ج: يعني أن المستهلك سيستقبل الرسالة نفسها أكثر من مرة في وقت ما، لذا يجب أن يكون كل أثر جانبي آمناً للتكرار. احمِ الآثار غير الـidempotent مثل الخصومات ورسائل البريد وطلبات POST الخارجية بسجل إزالة تكرار مفتاحه معرّف الرسالة يُكتب تحت قيد فريد قبل تنفيذ الأثر.

س: هل يمكن أن يشغّل Vercel Cron منتج طابور بدل أن ينفّذ العمل بنفسه؟

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

س: هل يغيّر Fluid Compute طريقة كتابة العمل الخلفي؟

ج: نعم، بطريقة محددة واحدة. يعيد Fluid Compute استخدام النسخ عبر الطلبات المتزامنة، فتكتمل الوعود المعلّقة بالصدفة غالباً، ما يجعل العمل الخلفي غير المملوك يبدو صحيحاً في الاختبار ويفشل بشكل متقطع في الإنتاج. سجّل دائماً العمل المؤجَّل صراحةً عبر waitUntil()‎ أو سلّمه لطابور.