proxy_cache_use_stale doesn't refresh cache timer when error responses continue
Environment
Running in a docker container, latest official build
# nginx -V
nginx version: nginx/1.29.3
built by gcc 14.2.0 (Debian 14.2.0-19)
built with OpenSSL 3.5.1 1 Jul 2025 (running with OpenSSL 3.5.4 30 Sep 2025)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/run/nginx.pid --lock-path=/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_v3_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -Werror=implicit-function-declaration -ffile-prefix-map=/home/builder/debuild/nginx-1.29.3/debian/debuild-base/nginx-1.29.3=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'
# uname -a
Linux 0143a472b060 6.10.14-linuxkit #1 SMP PREEMPT_DYNAMIC Sat May 17 08:29:19 UTC 2025 x86_64 GNU/LinuxDescription
I've set up my origin to return Cache-Control: max-age=60 with 429 Too Many Requests errors. These are triggered by a backend fetch to a 3rd party API. I want the error to be cached for a short period by nginx so that downstream requests don't keep repeatedly hitting the origin to reduce contribution to the rate limiting.
I'm trying to achieve this with the following directive:
proxy_cache_use_stale http_429;
The behaviour I'm seeing is:
T=00Request 1 -> 429 -> origin returns an error which is cachedT=10Request 2 -> HIT -> origin is not reachedT=70Request 3 -> STALE -> origin returns an error which is not cachedT=80Request 4 -> STALE -> origin returns an error which is not cached
Expected behaviour:
T=70Request 3 -> STALE -> origin returns an error which is cachedT=80Request 4 -> HIT -> origin is not reached
If I disable proxy_cache_use_stale I see this:
T=00Request 1 -> 429 -> origin returns an error which is cachedT=10Request 2 -> HIT -> origin is not reachedT=70Request 3 -> EXPIRED -> origin returns an error which is cachedT=80Request 4 -> HIT -> origin is not reached
Which is fine, but in the case where the cache has good data in it, I really would like the ability to serve it as stale in place of the 429.
I've tried various combinations of proxy_cache_use_stale proxy_cache_background_update proxy_cache_revalidate proxy_cache_valid to no avail.
I don't want to use proxy_ignore_headers because I want the origin to control the cache age for different endpoints.
- The bug is reproducible with the latest version of nginx
- The nginx configuration is minimized to the smallest possible to reproduce the issue and doesn't contain third-party modules
nginx configuration
upstream app_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response
server origin:8000 fail_timeout=0;
}
proxy_cache_path
/usr/share/nginx/cache
levels=1:2
keys_zone=disk_cache:10m
max_size=100m
inactive=60m
use_temp_path=off;
server {
listen ${NGINX_PORT};
server_name ${NGINX_HOST};
root /usr/share/nginx/html;
index index.html;
charset UTF-8;
location / {
try_files $uri @app;
}
location @app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://app_server;
proxy_cache disk_cache;
proxy_cache_lock on;
proxy_cache_lock_age 5s;
proxy_cache_lock_timeout 10s;
proxy_cache_use_stale http_429;
add_header "X-Cache-Status" $upstream_cache_status always;
add_header "X-Real-Status" $status always;
}
}Source: nginx/nginx