Skip to content

Add stale-while-revalidate support with on_stale refresh hook#139

Open
matthutchinson wants to merge 1 commit intosourcelevel:masterfrom
matthutchinson:support-stale-while-revalidate-async-cache-refreshing
Open

Add stale-while-revalidate support with on_stale refresh hook#139
matthutchinson wants to merge 1 commit intosourcelevel:masterfrom
matthutchinson:support-stale-while-revalidate-async-cache-refreshing

Conversation

@matthutchinson
Copy link

@matthutchinson matthutchinson commented Mar 24, 2026

Summary

  • Add support for the Cache-Control: stale-while-revalidate=<seconds> directive so stale cache entries can still be served during a bounded grace window.
  • Add a configurable :on_stale callback hook (proc.call(request:, env:, cached_response:)) that runs when a stale response is served, enabling callers to trigger asynchronous cache refreshes.
  • Adds test coverage, updates docs, and provides a runnable example showing background refresh behavior.

Related issue: #84

Why

Today, once a cached response becomes stale, the middleware must synchronously revalidate it before responding. That is correct for strict freshness, but it can increase latency and reduce resiliency for endpoints where slightly stale data is acceptable.

stale-while-revalidate is a standard HTTP cache directive that allows a better tradeoff:

  • callers get immediate responses from cache,
  • the cache can still be refreshed for subsequent requests.

See this explanation for more info, and this diagram.

695e180a27fcaea9b14c3267d3653501

This PR adds that behavior while preserving existing semantics for non-SWR responses.


How it works

1) Parse SWR directive

  • Faraday::HttpCache::CacheControl now exposes #stale_while_revalidate.

2) Determine SWR eligibility

  • Faraday::HttpCache::Response now exposes:
    • #stale_while_revalidate? → true when response is stale but still inside SWR window,
    • #stale_while_revalidate → parsed grace window in seconds.

3) Middleware request flow

  • Faraday::HttpCache now accepts :on_stale (or an equivalent init block).
  • In #process, behavior is now:
    • fresh entry: serve from cache (unchanged),
    • stale but SWR-eligible: serve cached response immediately, trace status as :stale, invoke on_stale callback,
    • otherwise stale: keep existing must_revalidate path (unchanged).

4) Callback contract

When serving a stale response in the SWR window, middleware invokes:

on_stale.call(request:, env:, cached_response:)

  • request: Faraday::HttpCache::Request
  • env: current Faraday::Env
  • cached_response: Faraday::HttpCache::Response

Callback errors are rescued and logged, so they do not break response serving.

5) Instrumentation/logging

  • Add :stale to cache statuses so instrumentation can distinguish SWR-served requests from fresh/valid hits.

Tests

Added/updated coverage for:

  • CacheControl SWR parsing,
  • response SWR eligibility logic,
  • middleware stale-serve + callback invocation + callback error safety,
  • behavior when SWR window has expired,
  • instrumentation status reporting as :stale.

Also added test server endpoints to exercise SWR flows.

Docs and example

  • Update README with:
    • SWR behavior explanation,
    • :on_stale usage and callback arguments,
    • :stale instrumentation status.
  • Add runnable example: examples/stale_while_revalidate.rb demonstrating stale serving and background refresh triggering.
  • Update changelog in Unreleased.

Backward compatibility

  • The new :on_stale arg is optional
  • Existing caching/revalidation behavior remains unchanged when stale-while-revalidate is not present.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant