{"id":1797,"date":"2025-11-13T19:47:50","date_gmt":"2025-11-13T16:47:50","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/serve-now-fix-quietly-how-stale-while-revalidate-and-stale-if-error-make-caching-feel-effortless-on-nginx-cloudflare-and-wordpress\/"},"modified":"2025-11-13T19:47:50","modified_gmt":"2025-11-13T16:47:50","slug":"serve-now-fix-quietly-how-stale-while-revalidate-and-stale-if-error-make-caching-feel-effortless-on-nginx-cloudflare-and-wordpress","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/serve-now-fix-quietly-how-stale-while-revalidate-and-stale-if-error-make-caching-feel-effortless-on-nginx-cloudflare-and-wordpress\/","title":{"rendered":"Serve Now, Fix Quietly: How stale-while-revalidate and stale-if-error Make Caching Feel Effortless on Nginx, Cloudflare, and WordPress"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, Saturday morning, sipping coffee and staring at logs while a client\u2019s site was on the front page of a big forum. Traffic spikes are fun\u2026 until they\u2019re not. The homepage held steady, but the product page buckled like a folding chair at a picnic. Here\u2019s the twist: users didn\u2019t feel a thing. They kept seeing fast pages, even while the origin tried to catch its breath. That\u2019s the magic of two tiny directives that feel like a cheat code: <strong>stale-while-revalidate<\/strong> and <strong>stale-if-error<\/strong>.<\/p>\n<p>If those sound like legalese from a dusty RFC, stick with me. They\u2019re actually a very human kind of safety net. Think of them as a friendly barista who hands you yesterday\u2019s pastry the moment you walk in\u2014so you\u2019re not hungry\u2014and then quietly bakes a fresh one in the back for your next visit. And if the bakery oven goes on the fritz? You still get something. In this post, I\u2019ll walk you through what these directives do, why they make sites feel snappy and resilient, and how I\u2019ve wired them up on Nginx, Cloudflare, and WordPress\u2014without turning your stack into a science experiment.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#The_Core_Idea_Why_Serving_Stale_Can_Be_the_Smartest_Move\"><span class=\"toc_number toc_depth_1\">1<\/span> The Core Idea: Why Serving \u201cStale\u201d Can Be the Smartest Move<\/a><\/li><li><a href=\"#How_Caches_Think_A_Short_Friendly_Primer\"><span class=\"toc_number toc_depth_1\">2<\/span> How Caches Think: A Short, Friendly Primer<\/a><\/li><li><a href=\"#Nginx_Your_Reliable_Short-Order_Cook\"><span class=\"toc_number toc_depth_1\">3<\/span> Nginx: Your Reliable Short-Order Cook<\/a><ul><li><a href=\"#What_I_like_about_Nginx_here\"><span class=\"toc_number toc_depth_2\">3.1<\/span> What I like about Nginx here<\/a><\/li><li><a href=\"#A_friendly_microcache_example\"><span class=\"toc_number toc_depth_2\">3.2<\/span> A friendly microcache example<\/a><\/li><li><a href=\"#Return_helpful_headers_from_the_origin\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Return helpful headers from the origin<\/a><\/li><li><a href=\"#A_quick_aside_on_timeouts_and_health\"><span class=\"toc_number toc_depth_2\">3.4<\/span> A quick aside on timeouts and health<\/a><\/li><\/ul><\/li><li><a href=\"#Cloudflare_Your_Bouncer_Butler_and_Butlers_Butler\"><span class=\"toc_number toc_depth_1\">4<\/span> Cloudflare: Your Bouncer, Butler, and Butler\u2019s Butler<\/a><ul><li><a href=\"#Headers_Cloudflare_understands\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Headers Cloudflare understands<\/a><\/li><li><a href=\"#A_simple_origin_header_strategy_for_CF\"><span class=\"toc_number toc_depth_2\">4.2<\/span> A simple origin header strategy for CF<\/a><\/li><\/ul><\/li><li><a href=\"#WordPress_Make_It_Friendly_Then_Make_It_Fast\"><span class=\"toc_number toc_depth_1\">5<\/span> WordPress: Make It Friendly, Then Make It Fast<\/a><ul><li><a href=\"#Sending_good_headers_from_WordPress\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Sending good headers from WordPress<\/a><\/li><li><a href=\"#Pairing_WordPress_with_Nginx_microcaching\"><span class=\"toc_number toc_depth_2\">5.2<\/span> Pairing WordPress with Nginx microcaching<\/a><\/li><li><a href=\"#About_purging_without_panic\"><span class=\"toc_number toc_depth_2\">5.3<\/span> About purging without panic<\/a><\/li><\/ul><\/li><li><a href=\"#Tuning_the_Dials_Numbers_that_Work_in_the_Real_World\"><span class=\"toc_number toc_depth_1\">6<\/span> Tuning the Dials: Numbers that Work in the Real World<\/a><ul><li><a href=\"#Preventing_thundering_herds\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Preventing thundering herds<\/a><\/li><li><a href=\"#Personalized_content_gotchas\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Personalized content gotchas<\/a><\/li><li><a href=\"#When_staleness_can_bite_you\"><span class=\"toc_number toc_depth_2\">6.3<\/span> When staleness can bite you<\/a><\/li><\/ul><\/li><li><a href=\"#Debugging_Trust_but_Verify\"><span class=\"toc_number toc_depth_1\">7<\/span> Debugging: Trust, but Verify<\/a><ul><li><a href=\"#What_I_usually_check\"><span class=\"toc_number toc_depth_2\">7.1<\/span> What I usually check<\/a><\/li><\/ul><\/li><li><a href=\"#Real-World_Patterns_I_Keep_Coming_Back_To\"><span class=\"toc_number toc_depth_1\">8<\/span> Real-World Patterns I Keep Coming Back To<\/a><ul><li><a href=\"#The_busy_homepage\"><span class=\"toc_number toc_depth_2\">8.1<\/span> The busy homepage<\/a><\/li><li><a href=\"#The_product_catalog\"><span class=\"toc_number toc_depth_2\">8.2<\/span> The product catalog<\/a><\/li><li><a href=\"#The_API_that_shouldnt_be_scary\"><span class=\"toc_number toc_depth_2\">8.3<\/span> The API that shouldn\u2019t be scary<\/a><\/li><\/ul><\/li><li><a href=\"#Putting_It_All_Together_Without_Overthinking_It\"><span class=\"toc_number toc_depth_1\">9<\/span> Putting It All Together Without Overthinking It<\/a><\/li><li><a href=\"#WrapUp_The_Quiet_Confidence_of_Serving_Stale\"><span class=\"toc_number toc_depth_1\">10<\/span> Wrap\u2011Up: The Quiet Confidence of Serving Stale<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"The_Core_Idea_Why_Serving_Stale_Can_Be_the_Smartest_Move\">The Core Idea: Why Serving \u201cStale\u201d Can Be the Smartest Move<\/span><\/h2>\n<p>Ever had that moment when your site is mostly fine, but one or two endpoints are just\u2026 tired? Not down, not broken\u2014just slow enough to make people feel it. That\u2019s where <strong>stale-while-revalidate<\/strong> shines. It says, \u201cIf the cached response is a little old, go ahead and serve it right now, then fetch a fresh copy in the background.\u201d Users get instant content. The cache gets updated quietly. Next visitor sees the fresh bake, but nobody waited in line.<\/p>\n<p>Now fold in <strong>stale-if-error<\/strong>, which is the cool-headed friend in a crisis. If your origin throws an error (maybe a 500, or a timeout), the cache is allowed to serve an older response rather than showing a scary error page. It\u2019s the difference between a small hiccup and a customer thinking your site is broken. I once had a client\u2019s database hiccup in the middle of a big promo. Thanks to stale-if-error, customers kept browsing, buying, and never knew we were scrambling behind the scenes.<\/p>\n<p>Here\u2019s the thing: most performance wins are about shaving milliseconds. These two directives are about <strong>protecting the user experience<\/strong> when things go sideways. They literally buy you time.<\/p>\n<h2 id=\"section-2\"><span id=\"How_Caches_Think_A_Short_Friendly_Primer\">How Caches Think: A Short, Friendly Primer<\/span><\/h2>\n<p>Let\u2019s keep this simple. When your app returns a response, it can include a <strong>Cache-Control<\/strong> header that tells caches what to do. Think of it like a polite note attached to the package. You might say \u201cthis is fresh for 60 seconds,\u201d or \u201cokay to reuse for a minute even if you\u2019re updating in the background,\u201d or \u201cif the kitchen burns down, give them yesterday\u2019s soup.\u201d<\/p>\n<p>The usual suspects in that note are <strong>max-age<\/strong> (how long the response is fresh), <strong>s-maxage<\/strong> (like max-age, but for shared caches like CDNs), <strong>stale-while-revalidate<\/strong> (allow serving stale while refreshing behind the scenes), and <strong>stale-if-error<\/strong> (serve stale if the origin fails). Not every cache honors every directive exactly the same way, but across Nginx, Cloudflare, and modern browsers and CDNs, these two have become practical tools you can count on. The key is to scope them wisely and test in your environment.<\/p>\n<p>In my experience, the most common mistake is overconfidence: folks slap an enormous stale window on everything and then wonder why a bug sticks around. Better to use short, predictable windows (think seconds or small minutes), and pair them with a solid purge story for when you must update immediately.<\/p>\n<h2 id=\"section-3\"><span id=\"Nginx_Your_Reliable_Short-Order_Cook\">Nginx: Your Reliable Short-Order Cook<\/span><\/h2>\n<h3><span id=\"What_I_like_about_Nginx_here\">What I like about Nginx here<\/span><\/h3>\n<p>Nginx is brutally efficient at being a shared cache in front of your app (or PHP-FPM). It gives you knobs for microcaching (tiny TTLs that make a big difference), background updates, and serving stale on errors. And it does all this without drama.<\/p>\n<p>When I\u2019m setting up an Nginx cache for WordPress or a custom app, I usually start with small max-age values (like 30\u201360 seconds), a <strong>stale-while-revalidate<\/strong> window in the same neighborhood, and a generous <strong>stale-if-error<\/strong> window (minutes to hours) because I want a long parachute if the origin is grumpy. Then I enable background updates and lock the cache to avoid a stampede.<\/p>\n<h3><span id=\"A_friendly_microcache_example\">A friendly microcache example<\/span><\/h3>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\"># Define a small, fast cache\nproxy_cache_path \/var\/cache\/nginx\/microcache levels=1:2 keys_zone=microcache:100m max_size=2g inactive=10m use_temp_path=off;\n\n# Optional: track upstream cache status for quick debugging\nmap $upstream_cache_status $cache_status {\n    default &quot;MISS&quot;;\n    HIT     &quot;HIT&quot;;\n    BYPASS  &quot;BYPASS&quot;;\n    EXPIRED &quot;EXPIRED&quot;;\n    STALE   &quot;STALE&quot;;\n    UPDATING &quot;UPDATING&quot;;\n}\n\nserver {\n    listen 80;\n    server_name example.com;\n\n    location \/ {\n        proxy_pass http:\/\/app;\n        proxy_cache microcache;\n        proxy_cache_key $scheme$proxy_host$request_uri;\n\n        # Serve stale if origin is sad; update in the background when possible\n        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;\n        proxy_cache_background_update on;\n        proxy_cache_lock on;\n        proxy_cache_lock_age 10s;\n        proxy_cache_lock_timeout 10s;\n\n        # Respect origin Cache-Control or override as needed\n        add_header X-Cache $cache_status always;\n    }\n}\n<\/code><\/pre>\n<p>With this setup, even if your app hits a rough patch, Nginx will <strong>serve stale content instead of errors<\/strong>, and it will refresh in the background for the next visitor. To really make the most of it, return Cache-Control headers from your app that include those two gems we\u2019ve been talking about.<\/p>\n<h3><span id=\"Return_helpful_headers_from_the_origin\">Return helpful headers from the origin<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: public, s-maxage=60, max-age=60, stale-while-revalidate=60, stale-if-error=86400\n<\/code><\/pre>\n<p>Give the cache a clear plan: one minute fresh, one minute to serve stale while refreshing, and one day parachute if your origin fails. You can tune those numbers to fit your content. For a busy blog homepage with frequent updates, I might use 30\/30\/3600. For product pages that change less often, I might push the error window much higher.<\/p>\n<p>If you want to go deeper on the knobs Nginx offers for serving stale responses and how it behaves under error conditions, the official docs are worth a read: <a href=\"https:\/\/nginx.org\/en\/docs\/http\/ngx_http_proxy_module.html#proxy_cache_use_stale\" target=\"_blank\" rel=\"noopener nofollow\">Nginx proxy_cache_use_stale and background update<\/a>.<\/p>\n<h3><span id=\"A_quick_aside_on_timeouts_and_health\">A quick aside on timeouts and health<\/span><\/h3>\n<p>Stale-if-error is amazing, but it\u2019s not a bandage for a sick origin. Make sure your timeouts, keep-alive, and network tuning are sane, especially during load. If that part makes you nervous, I\u2019ve shared how I keep long-lived connections and timeouts playing nicely behind a CDN in <a href=\"https:\/\/www.dchost.com\/blog\/en\/cloudflare-ile-websocket-ve-grpc-yayini-nasil-hep-canli-kalir-nginx-timeout-keep%e2%80%91alive-ve-kesintisiz-dagitimin-sirlari\/\">How I keep WebSockets and gRPC happy behind Cloudflare<\/a>, and if you manage big WordPress or Laravel sites, you may like <a href=\"https:\/\/www.dchost.com\/blog\/en\/yuksek-trafikli-wordpress-laravelde-linux-tcp-tuning-sysctl-ayarlari-udp-bufferlari-ve-syn-flooda-karsi-sakin-kalmak\/\">The Calm Guide to Linux TCP Tuning for High\u2011Traffic WordPress &amp; Laravel<\/a>.<\/p>\n<h2 id=\"section-4\"><span id=\"Cloudflare_Your_Bouncer_Butler_and_Butlers_Butler\">Cloudflare: Your Bouncer, Butler, and Butler\u2019s Butler<\/span><\/h2>\n<p>Cloudflare sits out front, keeping bad actors out and holding a huge cache close to your users. The neat part: you don\u2019t have to rewrite your app for Cloudflare to help. If your origin emits good Cache-Control headers\u2014including stale-while-revalidate and stale-if-error\u2014Cloudflare can honor them and keep pages flying even when your origin is moody.<\/p>\n<p>I like to think of Cloudflare as the friendly concierge who grabs the last known-good copy from the shelf when your kitchen is too busy to cook. In practice, this means two things. First, send your intent through headers. Second, test how your plan and settings behave in your zone. Features evolve, and Cloudflare does a lot of smart things by default (like serving stale on certain errors), but you should still verify your exact behavior with a simple checklist: what happens on 200, on 500, on timeout?<\/p>\n<h3><span id=\"Headers_Cloudflare_understands\">Headers Cloudflare understands<\/span><\/h3>\n<p>Generally speaking, Cloudflare plays well with Cache-Control directives like s-maxage, max-age, stale-while-revalidate, and stale-if-error. The more precise you are, the more predictable the cache becomes. You can read Cloudflare\u2019s take in their docs: <a href=\"https:\/\/developers.cloudflare.com\/cache\/concepts\/cache-control\/\" target=\"_blank\" rel=\"noopener nofollow\">Cloudflare and Cache-Control at the edge<\/a>.<\/p>\n<h3><span id=\"A_simple_origin_header_strategy_for_CF\">A simple origin header strategy for CF<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">Cache-Control: public, s-maxage=120, max-age=60, stale-while-revalidate=120, stale-if-error=86400\n<\/code><\/pre>\n<p>Here I\u2019m giving Cloudflare a longer leash (s-maxage=120) than browsers (max-age=60). Cloudflare serves from cache more aggressively than the end-user browser, which keeps your origin cooler. If the origin slows or fails briefly, visitors keep seeing a fast, cached page while the edge refreshes in the background or serves stale during an error.<\/p>\n<p>When I roll out this pattern, I like to observe real requests. Use curl or your browser developer tools to confirm the plan:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">curl -I https:\/\/example.com\/\n# Look for:\n#  - Cache-Control with stale-while-revalidate and stale-if-error\n#  - CF-Cache-Status (HIT, MISS, EXPIRED, etc.)\n<\/code><\/pre>\n<p>If you go further and put a load balancer in front of multiple origins, you may also find value in having a shared sense of health and error handling upstream. I\u2019ve written about that kind of setup in <a href=\"https:\/\/www.dchost.com\/blog\/en\/haproxy-ile-l4-l7-yuk-dengeleme-nasil-sifir-kesinti-sunar-health-check-sticky-sessions-ve-tls-passthroughu-sade-sade-konusalim\/\">Zero\u2011Downtime HAProxy: Layer 4\/7 Load Balancing<\/a>. Caching is one piece; good health checks and retries complete the picture.<\/p>\n<h2 id=\"section-5\"><span id=\"WordPress_Make_It_Friendly_Then_Make_It_Fast\">WordPress: Make It Friendly, Then Make It Fast<\/span><\/h2>\n<p>WordPress is an old friend, and like any old friend, it has quirks. It loves cookies, plugins, and dynamic bits. That\u2019s fine\u2014just be clear about what\u2019s cacheable. Usually, the best candidates are your homepage, category pages, and public posts for logged-out users. For logged-in users or cart pages, you bypass cache or get fancy with Edge Side Includes and fragment caching. Keep the first move simple: cache the anonymous experience well, and you win most of the battle.<\/p>\n<h3><span id=\"Sending_good_headers_from_WordPress\">Sending good headers from WordPress<\/span><\/h3>\n<p>If you run under PHP-FPM and Nginx (or even Apache behind Cloudflare), you can send headers right from WordPress for anonymous pages. Here\u2019s a minimal helper you can drop into a small must-use plugin or theme functions file. It\u2019s intentionally conservative:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ wp-content\/mu-plugins\/cache-headers.php\nadd_action('template_redirect', function () {\n    if (is_user_logged_in()) {\n        \/\/ Don\u2019t cache personalized views\n        return;\n    }\n\n    \/\/ Avoid caching cart\/checkout or any endpoint you consider dynamic\n    if (function_exists('is_woocommerce') &amp;&amp; (is_cart() || is_checkout() || is_account_page())) {\n        return;\n    }\n\n    $ttl = 60; \/\/ fresh for 60s\n    $swr = 60; \/\/ stale-while-revalidate for 60s\n    $sie = 86400; \/\/ stale-if-error for a day\n\n    header('Cache-Control: public, max-age=' . $ttl . ', s-maxage=' . $ttl . ', stale-while-revalidate=' . $swr . ', stale-if-error=' . $sie);\n});\n<\/code><\/pre>\n<p>This gives your CDN and Nginx a clear, consistent policy. Logged-in users, carts, and account pages skip the cache. Everyone else gets the speed and resilience boost.<\/p>\n<h3><span id=\"Pairing_WordPress_with_Nginx_microcaching\">Pairing WordPress with Nginx microcaching<\/span><\/h3>\n<p>One of my favorite combos is WordPress plus Nginx microcaching for 30\u201360 seconds. You\u2019d be amazed how much load disappears when a surge hits your homepage. Even if PHP dies for a minute, stale-if-error keeps visitors happy. And if you ever need instant updates\u2014like a breaking news site\u2014you can keep the stale windows short and wire a post-publish hook to purge just the affected paths or tags at the edge.<\/p>\n<p>Speaking of automation, if you\u2019re deep into infrastructure, I\u2019ve shared a practical path to \u201cboring and reliable\u201d deployments with Cloudflare integration in <a href=\"https:\/\/www.dchost.com\/blog\/en\/terraform-ile-vps-ve-dns-otomasyonu-cloudflare-proxmox-openstack-ve-sifir-kesinti-dagitim-nasil-bir-araya-gelir\/\">Automating VPS and zero\u2011downtime deploys with Terraform and Cloudflare<\/a>. It pairs nicely with predictable caching.<\/p>\n<h3><span id=\"About_purging_without_panic\">About purging without panic<\/span><\/h3>\n<p>Purges are like chainsaws\u2014powerful and dangerous in the wrong hands. Rather than blasting the entire cache every time you tweak a widget, purge by URL or by tag if your CDN supports it. Stale-while-revalidate is your friend here. You can purge the key path, let the first visitor trigger the background refresh, and everyone else gets a snappy stale copy while the fresh one bakes. No stampede, no drama.<\/p>\n<h2 id=\"section-6\"><span id=\"Tuning_the_Dials_Numbers_that_Work_in_the_Real_World\">Tuning the Dials: Numbers that Work in the Real World<\/span><\/h2>\n<p>Here\u2019s where the art meets the science. I don\u2019t treat stale windows as fire-and-forget; I tune them to the content and the risk. For a high-traffic blog homepage that updates frequently, 30\u201360 seconds of fresh, 30\u201360 seconds of stale-while-revalidate, and one hour of stale-if-error is a sweet spot. For a marketing site where content changes less often, I stretch those numbers. For a stock ticker or live scoreboard, I cut them down and rely more on purges.<\/p>\n<p>A quick sanity guide I use with clients: set <strong>stale-while-revalidate<\/strong> to about the time your origin needs to produce a fresh response under load. If your homepage rendering can spike to 2\u20134 seconds when traffic surges, give yourself 30\u201360 seconds of breathing room at the cache. For <strong>stale-if-error<\/strong>, think about your worst-case repair time\u2014enough to shield users while you roll back or fail over. If your failover is fast, keep it tight. If you know you might need an hour to fix a hot mess, be generous.<\/p>\n<h3><span id=\"Preventing_thundering_herds\">Preventing thundering herds<\/span><\/h3>\n<p>Ever watched a cache expire and your origin gets flooded by identical requests? That\u2019s the classic thundering herd. You avoid it by enabling cache locks (so one request refreshes while others wait) and background updates (so the updating happens behind the scenes while you serve stale). We already enabled those in the Nginx example with <strong>proxy_cache_lock<\/strong> and <strong>proxy_cache_background_update<\/strong>.<\/p>\n<h3><span id=\"Personalized_content_gotchas\">Personalized content gotchas<\/span><\/h3>\n<p>Cookies and cache don\u2019t always mix. WordPress likes to drop cookies for all kinds of reasons. That can make responses suddenly \u201cprivate\u201d and uncacheable if you\u2019re not careful. Keep your cache rules focused on truly public pages for anonymous users. Avoid caching responses that vary per user, or deliberately split the page into cacheable and non-cacheable fragments. If you feel the urge to cache everything, take a breath and resist. It\u2019s usually not worth the weird edge cases you\u2019ll inherit.<\/p>\n<h3><span id=\"When_staleness_can_bite_you\">When staleness can bite you<\/span><\/h3>\n<p>I once pushed a tiny theme change that broke a footer widget. No big deal, except the stale window kept that broken footer around a bit longer than we wanted. The fix was simple: shorter stale windows and a habit of targeted purges when rolling out changes that affect templates. Stale is a safety net, not an excuse to skip discipline in deployments.<\/p>\n<h2 id=\"section-7\"><span id=\"Debugging_Trust_but_Verify\">Debugging: Trust, but Verify<\/span><\/h2>\n<p>There\u2019s something oddly satisfying about confirming that your cache strategy is doing exactly what you intended. I keep a tiny checklist and use curl a lot.<\/p>\n<h3><span id=\"What_I_usually_check\">What I usually check<\/span><\/h3>\n<p>First, response headers:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">curl -I https:\/\/example.com\/\n# Look for Cache-Control containing:\n#   stale-while-revalidate=...\n#   stale-if-error=...\n# If using Cloudflare, also check:\n#   CF-Cache-Status: HIT | MISS | EXPIRED | STALE\n# If using Nginx, check your custom header:\n#   X-Cache: HIT | MISS | EXPIRED | STALE | UPDATING\n<\/code><\/pre>\n<p>Then I simulate trouble. I temporarily point the origin upstream to an invalid host or firewall it off (in a safe test environment!) to see if the cache serves stale content. When I see a fast response even during the induced failure, I know stale-if-error is working.<\/p>\n<p>If you like digging into the standards, the official definition of these directives lives here: <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc5861\" target=\"_blank\" rel=\"noopener nofollow\">RFC 5861: HTTP Cache Control Extension for Stale Content<\/a>. It\u2019s short, and surprisingly readable.<\/p>\n<h2 id=\"section-8\"><span id=\"Real-World_Patterns_I_Keep_Coming_Back_To\">Real-World Patterns I Keep Coming Back To<\/span><\/h2>\n<h3><span id=\"The_busy_homepage\">The busy homepage<\/span><\/h3>\n<p>News site or content-heavy homepage? Set fresh=30\u201360 seconds, stale-while-revalidate=30\u201360 seconds, stale-if-error=1\u20132 hours. Keep purges for breaking news. With microcaching in Nginx and edge caching in Cloudflare, your database will thank you.<\/p>\n<h3><span id=\"The_product_catalog\">The product catalog<\/span><\/h3>\n<p>For e-commerce, be strict about not caching carts, checkouts, and account pages. But product and category pages? Those are fair game for anonymous users. Tune stale-if-error high enough that a brief database burp doesn\u2019t interrupt browsing. Pair it with precise purges when inventory changes or prices update. You can layer this with an upstream load balancer that has sensible health checks; I\u2019ve laid out a calm path for that in <a href=\"https:\/\/www.dchost.com\/blog\/en\/haproxy-ile-l4-l7-yuk-dengeleme-nasil-sifir-kesinti-sunar-health-check-sticky-sessions-ve-tls-passthroughu-sade-sade-konusalim\/\">Zero\u2011Downtime HAProxy: Layer 4\/7 Load Balancing<\/a>.<\/p>\n<h3><span id=\"The_API_that_shouldnt_be_scary\">The API that shouldn\u2019t be scary<\/span><\/h3>\n<p>Even APIs benefit from carefully scoped caching. Public endpoints that don\u2019t require authentication can safely use small fresh windows with stale-while-revalidate. If a backend dependency goes sideways, stale-if-error is your friend. Just make sure you version your endpoints and have a clear purge story. And yes, mind your timeouts and keep-alive\u2014if you\u2019re curious about a stable setup behind Cloudflare, have a look at <a href=\"https:\/\/www.dchost.com\/blog\/en\/cloudflare-ile-websocket-ve-grpc-yayini-nasil-hep-canli-kalir-nginx-timeout-keep%e2%80%91alive-ve-kesintisiz-dagitimin-sirlari\/\">this friendly guide on timeouts and zero\u2011downtime behind Cloudflare<\/a>.<\/p>\n<h2 id=\"section-9\"><span id=\"Putting_It_All_Together_Without_Overthinking_It\">Putting It All Together Without Overthinking It<\/span><\/h2>\n<p>If this all feels like a lot, breathe. You can get 80% of the win with a simple, consistent pattern:<\/p>\n<p>First, decide what\u2019s cacheable for anonymous users. Second, have your origin send headers with short, predictable fresh times and small stale-while-revalidate windows. Third, enable stale-if-error to keep users safe during bumps. Fourth, make sure your proxy (Nginx) is set to serve stale and refresh in the background with cache locks to prevent stampedes. Finally, confirm that your CDN (Cloudflare) is seeing and respecting the plan, and build a habit of surgical purges instead of nuclear ones.<\/p>\n<p>Once you\u2019ve got that rhythm, you can refine. Roll in cache tagging. Add smart purges on publish events. Observe how your app behaves under load and adjust the windows. And when you\u2019re tuning the wider platform\u2014network buffers, TLS, timeouts\u2014do it calmly and methodically. If you like that mindset, I wrote about it in <a href=\"https:\/\/www.dchost.com\/blog\/en\/yuksek-trafikli-wordpress-laravelde-linux-tcp-tuning-sysctl-ayarlari-udp-bufferlari-ve-syn-flooda-karsi-sakin-kalmak\/\">this no\u2011drama guide to Linux TCP tuning for WordPress &amp; Laravel<\/a>.<\/p>\n<h2 id=\"section-10\"><span id=\"WrapUp_The_Quiet_Confidence_of_Serving_Stale\">Wrap\u2011Up: The Quiet Confidence of Serving Stale<\/span><\/h2>\n<p>Here\u2019s what I\u2019ve learned after many late nights and a few too many coffees: speed isn\u2019t just about faster servers; it\u2019s about <strong>grace under pressure<\/strong>. stale-while-revalidate and stale-if-error give your stack a kind of hospitality. They keep visitors comfortable while you handle the kitchen. They smooth out traffic spikes, soften backend hiccups, and buy you precious time to fix things the right way instead of the fast way.<\/p>\n<p>If you\u2019re rolling this out today, start small. Pick your most visited public pages. Add friendly Cache-Control headers with modest windows. Enable Nginx\u2019s background updates and stale serving. Confirm Cloudflare is honoring your intent. Watch your logs, tweak the dials, and don\u2019t forget to set up a sane purge workflow. If your stack includes Cloudflare, load balancers, and a few moving parts, keep the rest of your config boring and predictable\u2014timeouts, health checks, and deploys matter. When you\u2019re ready to automate more, consider a calm, repeatable pipeline like the one I shared in <a href=\"https:\/\/www.dchost.com\/blog\/en\/terraform-ile-vps-ve-dns-otomasyonu-cloudflare-proxmox-openstack-ve-sifir-kesinti-dagitim-nasil-bir-araya-gelir\/\">Automating VPS and zero\u2011downtime deploys with Terraform and Cloudflare<\/a>.<\/p>\n<p>Serve users now, refresh quietly, and let errors roll off your back. That\u2019s the kind of resilience your visitors feel\u2014without ever knowing why. Hope this was helpful! See you in the next post, and may your caches be warm and your coffee strong.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, Saturday morning, sipping coffee and staring at logs while a client\u2019s site was on the front page of a big forum. Traffic spikes are fun\u2026 until they\u2019re not. The homepage held steady, but the product page buckled like a folding chair at a picnic. Here\u2019s the twist: users didn\u2019t feel a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1798,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1797","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1797","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1797"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1797\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1798"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1797"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1797"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1797"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}