Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why how

Why does CORS exist?

CORS isn't there to keep you out of an API — it's there to stop a webpage you're visiting from quietly using your logged-in cookies on a different site. The whole design only makes sense once you see that.

Networking intro Apr 29, 2026

Why it exists

Every web engineer has had this moment: you fetch() an API from your frontend, the request looks fine, but the browser refuses it with a wall of red console text mentioning CORS. The natural reaction is “why is the browser blocking my own request to my own server?”

The answer is the part that’s almost always skipped in the error message: CORS is not protecting the server. It’s protecting you, the user, from the page you’re currently looking at.

Here’s the threat model. You’re logged into your bank in one tab — your bank’s session cookie is sitting in the browser. You open another tab and visit evil.example. Without any browser rules, JavaScript on evil.example could just call fetch("https://yourbank.com/api/transfer?..."). The browser, being helpful, would attach your bank’s cookies to the request because that’s what browsers do for yourbank.com. The bank would see a perfectly authenticated request and act on it. Done — your money is gone, and you didn’t click anything other than a link.

This is why the browser enforces the same-origin policy: JS running on origin A cannot, by default, read responses from origin B. Origin here means the triple (scheme, host, port)https://api.foo.com and https://foo.com are different origins.

CORS is the opt-in escape hatch on top of that policy. It lets a server on origin B say “actually, JS from origin A is welcome to read my responses.” The default is no; CORS is how the server says yes.

Why it matters now

Almost every modern app is split across origins: a SPA on app.example.com talking to an API on api.example.com, a static site on a CDN calling a backend somewhere else, a third-party widget embedded in a customer page. Every one of those crossings goes through CORS.

It also shows up the moment you build anything with an LLM in the browser — calling a model provider’s API directly from a webpage runs straight into CORS, which is exactly the point. The browser is making sure a random page can’t burn your API credits using a key that leaked into localStorage.

If CORS feels annoying, it’s because the threat it blocks is invisible: you never see the requests it stopped, only the legitimate ones it got in the way of.

The short answer

CORS = same-origin policy + a server-side opt-in via response headers

The browser refuses cross-origin reads by default. CORS is the protocol where the server explicitly says “this origin may read this response,” using HTTP headers like Access-Control-Allow-Origin. The browser, not the server, is the one enforcing the result.

How it works

Two things are happening, and conflating them is what makes CORS confusing.

1. Simple requests. A “simple” cross-origin request — roughly, a GET or basic POST with safe headers — actually goes out to the server. The server processes it and returns a response. Then the browser looks at the response headers. If Access-Control-Allow-Origin doesn’t include the calling origin, the browser throws away the response before the JS can read it. The request hit the server; the JS just isn’t allowed to see the answer.

This is worth pausing on, because it surprises people: CORS doesn’t stop the request from happening. It stops the page from learning what came back. For side-effecting endpoints, that’s not enough on its own — which is why servers also need CSRF protection (e.g. SameSite cookies, CSRF tokens). CORS and CSRF defenses are neighbors, not substitutes.

2. Preflighted requests. Anything more dangerous — custom headers, content types other than the basic three, methods like PUT/DELETE — triggers a preflight. Before the real request, the browser sends an OPTIONS request asking “may JS on origin A make a PUT to this URL with these headers?” The server answers with Access-Control-Allow-* headers. Only if the answer is yes does the browser send the real request at all.

A preflight in cartoon form:

Browser → server:  OPTIONS /api/things
                   Origin: https://app.example
                   Access-Control-Request-Method: PUT
                   Access-Control-Request-Headers: x-auth

Server  → browser: 204 No Content
                   Access-Control-Allow-Origin: https://app.example
                   Access-Control-Allow-Methods: PUT
                   Access-Control-Allow-Headers: x-auth
                   Access-Control-Max-Age: 600

Browser → server:  PUT /api/things   (the real request)

Max-Age lets the browser cache that “yes” so it isn’t re-asking before every call. The exact upper bound varies by browser — I don’t have a current authoritative number to quote here, and it has changed over time, so check your target browsers if it matters.

Credentials are a separate switch. Cookies and Authorization headers aren’t sent on cross-origin requests unless the JS explicitly asks (credentials: "include" in fetch) and the server explicitly allows it (Access-Control-Allow-Credentials: true). And when credentials are in play, the server cannot reply with Access-Control-Allow-Origin: * — it has to echo the specific origin. That’s not a quirk; it’s the whole point. Wildcard plus cookies would re-open the bank-tab attack.

The seam to notice: enforcement lives in the browser. A non-browser client (curl, your backend, a mobile app) ignores CORS entirely, because the threat model — “an attacker page running in the user’s authenticated session” — doesn’t apply there. CORS protects browser users, not servers. A server that treats Access-Control-Allow-Origin as access control has misunderstood what it does.

Going deeper