CORS preflight caching: how Access-Control-Max-Age works
Every non-simple cross-origin request should trigger one preflight and then reuse that answer, not send a fresh OPTIONS before every call. Access-Control-Max-Age is the header that makes that happen, but browsers cap how long they honor it, and it's easy to set it and still see an OPTIONS on every request.
What a missed cache costs
A preflight is a full round trip before the request you wanted to send: the browser sends OPTIONS, the server answers, then the real GET or POST goes out. On a fast connection that's a rounding error. On a mobile network, or against a distant API, it adds a whole extra round trip to every call that needs one, and a JSON API sending an Authorization header needs one for every distinct request shape.
If your app makes the same kind of call over and over (same origin, same URL, same method, same headers), the browser only needs to ask once. Access-Control-Max-Age is how the server tells it how long "once" lasts.
The header, and its default
It goes on the OPTIONS reply, not the real response:
OPTIONS /v1/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
The value is the number of seconds the browser may reuse this preflight result. If the header is missing, the default is 5 seconds, which rarely spans two real requests from a user.
Browsers cap the value you send
Per MDN's Access-Control-Max-Age reference, Chromium (since Chrome 76) caps the effective value at 7200 seconds (2 hours) and Firefox at 86400 seconds (24 hours). Safari applies its own cap too; we haven't found a source we trust for its exact value, so test in Safari rather than assume. No browser tells you it clamped the value: the response header shows what you sent, and the cache lifetime is the browser's ceiling. Asking for more than 7200 buys nothing extra in Chrome.
What invalidates the cache
The cached answer is keyed to the requesting origin and the request URL, and records which methods and headers were approved. A later call can skip the preflight only if its method and every one of its headers were covered. Add a header you didn't preflight for, switch from GET to DELETE, or hit a different path, and you get a fresh OPTIONS even with time left on the cache. An app that "randomly" preflights every few calls is usually varying something in the request, such as an extra debug header or a conditionally added Authorization.
While you're debugging CORS config, send Access-Control-Max-Age: 0 so a stale cached answer can't mask your edits.
What corsproxy.dev sends
The proxy's own OPTIONS responses set Access-Control-Max-Age: 86400, matching Firefox's ceiling. Chrome shortens it to two hours and Safari applies its own limit; nothing on the server side gets past those. If you're fronting your own API, a value between 7200 and 86400 is enough.
Checking the cache works
Open DevTools → Network, filter by "Fetch/XHR", and make the same call twice in quick succession. The first should show an OPTIONS followed by the real request; the second should show only the real request. If you see an OPTIONS every time, first check that response for Access-Control-Max-Age (a missing header, and so the 5-second default, is the usual cause), then check that the repeated request is identical in method and headers to the one that got cached.