NGINX proxy caching missing `Age` header

My issue: We are running nginx as an API gateway in a multi-layer caching infrastructure. API calls from clients have to pass trough 2 layers of cache, namely:

  • a CDN operated by cloudflare
  • Nginx acting as API gateway

The cache feature on nginx is missing the Age header when serving assets from its cache, not respecting RFC 7234 section 4.2.3.

As per the RFC, this header helps cloudflare (downstream client) to calculate the time left to cache the assets on its side. However, this header not being present causes sometimes cloudflare to cache the assets for twice the max-age set in the Cache-Control header.

Nginx also does not take into account the Age header it receives from upstream when calculating the left max-age to cache assets.

How I encountered the problem:

Some assets on cloudflare could be cached for a maximum of twice the max-age when the Age header is missing and depending on when cloudflare is asking for the assets to nginx.

Nginx as well might store the assets loner than expected, not considering upstream Àge` header which tells him how long the upstream cache stored the asset already.

Solutions I’ve tried: N/A

Version of NGINX or NGINX adjacent software (e.g. NGINX Gateway Fabric): 1.27

Deployment environment:

Docker compose environment

Summary
services:
  curl:
    image: nicolaka/netshoot
    restart: always
    command: sleep 10000

  nginx:
    image: "nginx:1.27"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "8080:8080"

  upstream:
    image: ealen/echo-server:0.9.2
    environment:
      PORT: 80

Nginx configuration

Summary
pid /tmp/nginx.pid;

worker_processes     1;
worker_rlimit_nofile 8192;

events {
    worker_connections 4096;
}

http {
    include /etc/nginx/mime.types;
    index   index.html index.htm index.php;

    log_format custom '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '"cache:$upstream_cache_status" '
                '"age:$upstream_http_age"';

    proxy_cache_path /etc/nginx/cache keys_zone=mycache:10m;
    

    server {
        listen      80;
        server_name _;
        access_log  /dev/stdout custom;
        proxy_cache mycache;

        location / {
            proxy_pass http://upstream;
            add_header 'x-cache-status' $upstream_cache_status always;
        }
    }
}

#1 Nginx caching the asset and not sending Age header

Using the curl container we issue the following commands

$ curl http://nginx -X GET -I -H 'X-ECHO-HEADER: cache-control:public,max-age=10'
HTTP/1.1 200 OK
Server: nginx/1.27.5
Date: Fri, 01 Aug 2025 14:31:16 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 536
Connection: keep-alive
cache-control: public,max-age=10
ETag: W/"218-YdPQfL3An/Yz++FIFyecImCtdIk"
x-cache-status: MISS


$ curl http://nginx -X GET -I -H 'X-ECHO-HEADER: cache-control:public,max-age=10'
HTTP/1.1 200 OK
Server: nginx/1.27.5
Date: Fri, 01 Aug 2025 14:31:18 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 536
Connection: keep-alive
cache-control: public,max-age=10
ETag: W/"218-YdPQfL3An/Yz++FIFyecImCtdIk"
x-cache-status: HIT
nginx-1  | 172.19.0.4 - - [01/Aug/2025:14:31:16 +0000] "GET / HTTP/1.1" 200 536 "-" "curl/8.14.1" "cache:MISS" "age:-"
nginx-1  | 172.19.0.4 - - [01/Aug/2025:14:31:18 +0000] "GET / HTTP/1.1" 200 536 "-" "curl/8.14.1" "cache:HIT" "age:-"

Nginx is not sending any Age header to the downstream during a HIT. We have no way of knowing how long the asset’s time has left.

#2 Nginx caching the asset and not respecting upstream Age header

Using the curl container we issue the following commands

$ curl http://nginx -X GET -I -H 'X-ECHO-HEADER: cache-control:public,max-age=10, age:9'
HTTP/1.1 200 OK
Server: nginx/1.27.5
Date: Fri, 01 Aug 2025 14:34:43 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 543
Connection: keep-alive
cache-control: public,max-age=10
age: 9
ETag: W/"21f-CNUY6UVipFWmDQJaPkebkFa2DD4"
x-cache-status: EXPIRED

$ curl http://nginx -X GET -I -H 'X-ECHO-HEADER: cache-control:public,max-age=10, age:9'
HTTP/1.1 200 OK
Server: nginx/1.27.5
Date: Fri, 01 Aug 2025 14:34:44 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 543
Connection: keep-alive
cache-control: public,max-age=10
age: 9
ETag: W/"21f-CNUY6UVipFWmDQJaPkebkFa2DD4"
x-cache-status: HIT

# Sent 5s later

$ curl http://nginx -X GET -I -H 'X-ECHO-HEADER: cache-control:public,max-age=10, age:9'
HTTP/1.1 200 OK
Server: nginx/1.27.5
Date: Fri, 01 Aug 2025 14:34:49 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 543
Connection: keep-alive
cache-control: public,max-age=10
age: 9
ETag: W/"21f-CNUY6UVipFWmDQJaPkebkFa2DD4"
x-cache-status: HIT
nginx-1  | 172.19.0.4 - - [01/Aug/2025:14:34:43 +0000] "GET / HTTP/1.1" 200 543 "-" "curl/8.14.1" "cache:EXPIRED" "age:9"
nginx-1  | 172.19.0.4 - - [01/Aug/2025:14:34:44 +0000] "GET / HTTP/1.1" 200 543 "-" "curl/8.14.1" "cache:HIT" "age:9"
nginx-1  | 172.19.0.4 - - [01/Aug/2025:14:34:49 +0000] "GET / HTTP/1.1" 200 543 "-" "curl/8.14.1" "cache:HIT" "age:9"

Here even if we issue the 3rd curl 5s after the 2nd curl we still have a HIT. However nginx should have computed the Age header sent by the upstream and only cached the assets for 10 - 9 = 1s.

NGINX access/error log: (Tip → You can usually find the logs in the /var/log/nginx directory.) N/A

Hey @maximumG! Can I ask you to open a discussion on the NGINX GitHub repo? This seems something that might benefit from getting the developers to weight in.

In the meantime, a quick Google search shows some potential work arounds, but I understand why that might not be ideal.

Hey @alessandro. Thanks for your quick feedback on this, really appreciated :slightly_smiling_face:.

Actually I wasn’t sure in the first place about opening a Github discussion. I’ll do it based on your advise.

In the meantime, I just wanted to share this with the community to check is someone else is having the same issue as we do.

We already considered some options here, even going with openresty but would like to stay on vanilla nginx.

1 Like

Here is the GH discussion if someone wants to follow-up on this: NGINX proxy caching missing `Age` header · nginx/nginx · Discussion #821 · GitHub

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.