A browser sends a CORS preflight, an OPTIONS request, before any cross-origin request that is not "simple". Methods other than GET, HEAD or POST, a custom or Authorization header, or a JSON content type all trigger one. The server's reply decides whether the real request is sent, and Access-Control-Max-Age controls how long that answer is cached.
What counts as a simple request
A request skips the preflight only if all of these hold, per MDN:
- The method is GET, HEAD or POST.
- Apart from headers the browser sets itself, only safelisted headers are set:
Accept,Accept-Language,Content-Language,Content-Type(with limits) andRangewith a single range. - If there is a
Content-Type, it isapplication/x-www-form-urlencoded,multipart/form-dataortext/plain. - No upload event listeners on an XMLHttpRequest, and no ReadableStream body.
That list is why a fetch that posts application/json triggers a preflight, even though the method is POST. It is also why adding an Authorization header does.
What the exchange looks like
The browser sends OPTIONS with Access-Control-Request-Method and, when relevant, Access-Control-Request-Headers. The server answers with what it allows:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Only then does the browser send the real request. Preflights never carry credentials. If the real request will include cookies, the preflight response must also say Access-Control-Allow-Credentials: true.
Caching with Max-Age
Access-Control-Max-Age gives the number of seconds the browser may reuse the preflight answer. MDN states the default is 5 seconds, which means a busy page may preflight constantly. Setting it to 86400 allows a day. Each browser enforces its own internal maximum, which takes precedence when your value is higher. MDN does not give the cap numbers, so test in the browsers you support rather than assuming.
Longer caching has a cost. If you tighten the allowed headers, clients that cached the old answer may keep using it until it expires.
Common mistakes
- Handling OPTIONS behind authentication, so the preflight gets a 401. Preflights carry no credentials.
- Forgetting a header in
Access-Control-Allow-Headers, which fails only for the requests that use it. - Treating the preflight as a security control. It protects browsers from sending certain requests, but a script outside a browser ignores it, so your server must still authorise every request.
A debugging habit
Open the network panel, find the OPTIONS request, and compare the request headers with the response. The browser console error usually names the exact header or method that was not allowed. Fix that single item rather than widening everything.
Quick check
Why does my JSON POST send a preflight?
Because application/json is not one of the three safelisted content types, the request is not simple.
Does a preflight protect my API?
No. It is a browser behavior. Any non-browser client can skip it, so authorise on the server.
Sources
Last reviewed: 19 October 2026.