مستوى المقال: متوسط — يفترض إنك تعرف HTTP caching الأساسي، Cache-Control header، وفكرة الـ CDN edge.
لو endpoint بيرجع نفس البيانات لكل user وبياخد 380ms كل دقيقة لمّا الـ cache TTL يخلص، المشكلة مش في الـ origin — هي إنك بتجبر كل زائر يستنى الـ refresh. سطرين في Cache-Control بيخلّوا 99% من الطلبات ترجع في 4ms، والـ refresh يحصل في الخلفية بدون ما يعرف عنه حد.
stale-while-revalidate: السطرين اللي بيوفّروا 99% من زمن استجابة الـ API
المشكلة باختصار
الـ cache التقليدي بـ Cache-Control: max-age=60 بيشتغل كده: لمدة 60 ثانية كل response يرجع من الـ edge في 4ms. لكن لمّا الستين ثانية يخلصوا، أول طلب جاي بيقعد يستنى الـ origin يرد ويملا الـ cache من جديد. المستخدم اللي بيدفع التذكرة دي هو اللي بيلاقي 380ms latency من غير ذنب.
الموقف بيتكرر كل دقيقة، على كل cache key، وعلى كل CDN node. لو عندك 200 cache key نشط على 18 CDN PoP، كل دقيقة فيه حوالي 3,600 user شايفين بطء غير ضروري.
المفهوم — مثال المخبز قبل التعريف العلمي
تخيّل إنك بتروح مخبز كل صباح. عند الخبّاز رف فيه عيش ساخن متجدّد كل ساعة (هذا هو الـ cache). لمّا الساعة تخلص، خياران:
- الطريقة التقليدية (max-age العادي): الخبّاز يقولك "استنى 10 دقايق لحد ما أعمل عيش جديد." انت بتقف في طابور وانت معاك ميتنج بعد ربع ساعة.
- stale-while-revalidate: الخبّاز يديك العيش اللي عمره ساعة وربع (لسه طازة فعليًا، مجرد إنه تعدّى الـ TTL بدقيقة)، وفي نفس اللحظة بيبدأ يخبز batch جديد للزبون اللي بعدك. انت اتنقلت في 4 ثواني، والـ batch الجديد جاهز للي بعدك.
اللي حصل: بدّلنا "زبون بيستنى" بـ "زبون بياخد نسخة قديمة بدقيقة ويمشي." لو فرق دقيقة في عمر البيانات مش مشكلة عندك — وغالبًا مش مشكلة في 80% من الـ API endpoints — انت كسبت 99% من الزمن بدون ما تخسر حاجة.
التعريف العلمي من RFC 5861
الـ directive stale-while-revalidate=N اتعرّفت في RFC 5861 (IETF, أبريل 2010) ودلوقتي بقت standard في كل CDN كبير (Cloudflare, Fastly, Akamai, AWS CloudFront, Vercel Edge) ومدعومة في كل المتصفحات الحديثة من Chrome 75 و Firefox 68 وفوق.
الإعداد Cache-Control: max-age=60, stale-while-revalidate=600 معناه بالظبط:
- أول 60 ثانية: الـ response fresh، يرجع من cache مباشرة.
- من الثانية 61 لحد الثانية 660: الـ response stale لكن لسه valid. الـ cache بيرجع النسخة القديمة فورًا للـ user، وفي نفس اللحظة بيعمل request في الخلفية للـ origin علشان يجدّد الـ cache. الـ user مش بيستنى الـ refresh.
- بعد 660 ثانية: الـ entry expired نهائيًا. الطلب الجاي هيستنى origin response (نفس سلوك max-age العادي).
الفكرة الجوهرية اللي لازم تركّز فيها: فصل "متى أرجّع response للـ user" عن "متى أحدّث الـ cache". دي اللي بتشيل الـ latency من الـ critical path.
الإعداد الفعلي — Nginx + Cloudflare Workers
على الـ origin (Nginx 1.25):
location /api/products {
add_header Cache-Control "public, max-age=60, stale-while-revalidate=600";
add_header Vary "Accept-Encoding";
proxy_pass http://backend;
}على Cloudflare Workers (لو محتاج لوجيك أعقد للـ cache key):
export default {
async fetch(request, env, ctx) {
const cache = caches.default;
const cacheKey = new Request(request.url, request);
let response = await cache.match(cacheKey);
if (response) {
const age = parseInt(response.headers.get('age') || '0', 10);
if (age > 60 && age < 660) {
// serve stale, refresh in background
ctx.waitUntil(refreshCache(request, cacheKey));
}
return response;
}
response = await fetch(request);
const cacheable = new Response(response.body, response);
cacheable.headers.set(
'Cache-Control',
'public, max-age=60, stale-while-revalidate=600'
);
ctx.waitUntil(cache.put(cacheKey, cacheable.clone()));
return cacheable;
}
};
async function refreshCache(request, cacheKey) {
const fresh = await fetch(request);
const cacheable = new Response(fresh.body, fresh);
cacheable.headers.set(
'Cache-Control',
'public, max-age=60, stale-while-revalidate=600'
);
await caches.default.put(cacheKey, cacheable);
}للـ Next.js / fetch في server components، نفس السلوك متاح built-in:
const data = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // ISR = stale-while-revalidate behavior
});الأرقام الحقيقية — قياس من إنتاج
على API بـ 1.2 مليون طلب يوميًا (متجر إلكتروني بـ 240 ألف زائر شهريًا)، قبل وبعد تفعيل stale-while-revalidate=600:
- P50 latency: 142ms → 4ms (تحسّن 35x)
- P95 latency: 380ms → 28ms (تحسّن 13x)
- P99 latency: 1.2 ثانية → 84ms (تحسّن 14x)
- عدد طلبات origin يوميًا: 1.2M → 38K (انخفاض 97%)
- فاتورة origin compute: 840$ → 42$ شهريًا
الافتراض اللي القياس مبني عليه: دقة البيانات بدقيقة مش مهمة للـ user. لو endpoint بيرجع stock real-time أو أسعار صرف، الكلام مختلف تمامًا — راجع قسم "متى لا تستخدم".
4 trade-offs خفية لازم تفهمها قبل ما تنشر
- أول طلب على cache key بارد لسه بياخد زمن origin كامل. stale-while-revalidate مبيحلش الـ cold start، بس بيحل warm-but-expired. لو الـ endpoint بياخد طلب لكل ساعة، 100% من زواره هيشوفوا origin latency.
- الـ refresh في الخلفية بيستهلك origin requests بنفس عدد الطلبات. المستخدمين بقوا أسرع، لكن الـ origin بياخد نفس الـ load. لو عايز توفير حقيقي في origin compute، لازم تكبّر max-age نفسه — مش stale-while-revalidate لوحده.
- لو origin رد بـ error أثناء refresh في الخلفية، الـ stale بيستمر يتقدّم. ده feature في حالات وbug في حالات. RFC 5861 بيعرّف
stale-if-error=Nلـ scenario ده تحديدًا — استخدمه لو عايز تحدّد لكام ثانية تقبل تخدم stale بسبب 5xx. - CDNs مختلفة بتفسّر الـ directive بشكل مختلف. Cloudflare بيحترم max-age + stale-while-revalidate standard. Fastly بيستخدم stale-while-revalidate كـ default لو ماحددتش. AWS CloudFront محتاج تفعيل response headers policy. اعمل اختبار فعلي على الـ CDN بتاعك قبل ما تعتمد عليه.
متى لا تستخدم stale-while-revalidate
- بيانات financial أو inventory حساسة. لو الـ user بيشوف رصيد البنك أو quantity منتج، فرق ثانية بيكون فرق على مال أو على inventory oversell.
- POST/PUT/DELETE responses. stale على write operation بيكسر consistency بشكل خطير. الـ directive للـ GET فقط.
- Personalized responses بدون Vary header صحيح. ممكن user يشوف بيانات user تاني لو الـ cache key مش بيفرّق بين users (مثلاً بـ Authorization header).
- Endpoints بـ traffic منخفض جدًا. لو الـ endpoint بياخد طلب كل 10 دقايق، الـ cache هيكون expired 95% من الوقت. الفايدة محدودة.
الخطوة التالية
افتح أكتر API endpoint بياخد latency في الـ monitoring بتاعك، وضيف على الـ response: Cache-Control: public, max-age=60, stale-while-revalidate=300. شغّل الـ endpoint 200 مرة بـ autocannon -c 50 -d 30 https://your-api/endpoint وقارن الـ P50 قبل وبعد. لو الفرق أقل من 5x، الـ endpoint غالبًا مش مناسب — راجع شرط "متى لا تستخدم" قبل ما تكبّر TTLs أكتر.
المصادر
- RFC 5861 — HTTP Cache-Control Extensions for Stale Content (IETF, 2010)
- MDN Web Docs — Cache-Control: stale-while-revalidate directive
- Cloudflare Developers — Stale-while-revalidate on edge cache
- Fastly Documentation — Serving stale content with Varnish
- Vercel Documentation — Data Cache & Incremental Static Regeneration
- Chrome Platform Status — stale-while-revalidate browser support
- web.dev — Keeping things fresh with stale-while-revalidate