401 means the server does not know you. 403 means it does.

30/07/2026 · Laurent HTTP status codes
401 means the server does not know you. 403 means it does.

The two codes answer different questions, and the labels do not help.

401 Unauthorized means the server does not know who you are. It is an invitation: authenticate and try again. The specification calls it unauthorized, which is misleading enough that RFC 9110 says so in its own text, noting the name is retained for compatibility while the meaning is unauthenticated.

403 Forbidden means the server knows exactly who you are, or has decided it does not need to, and the answer is no. Repeating the request with credentials will not change anything. There is nothing to try again.

Getting these two the wrong way round is not a pedantic concern. It decides which file you open. A 401 sends you to credentials, to a session, to a token that expired. A 403 sends you to rules: a permission, a firewall, a file mode, a plugin that decided this request looked wrong.

What makes a 401 a real 401

A 401 is only valid if it carries a WWW-Authenticate header naming the scheme. That header is the invitation. Without it the response says "you are not authenticated" and gives the client no way to become authenticated, which is a 403 wearing the wrong number.

curl -sSI https://example.com/ | grep -iE 'HTTP/|www-authenticate'

Three shapes come back in practice.

WWW-Authenticate: Basic realm="Staging" is a server-level guard, almost always Apache or nginx sitting in front of everything. The application has not run. Nothing in the site's own logs will mention this request.

WWW-Authenticate: Bearer with an error= parameter is an API telling you the token is missing, expired, or scoped wrong, and the parameter usually says which.

No header at all, and a themed HTML page: the application produced this. Some frameworks and a great many WordPress plugins return 401 for "you are logged in but this is not yours", which is the definition of 403. You now know the code is unreliable on this site and you should read the body instead.

The three 403s you will actually meet on client sites

403 is the most overloaded code in production, because four separate layers can emit it and none of them coordinate.

ShapeUsually meansTell
Plain server page, Apache or nginx signatureFile permissions, a directory with no index, or a deny rule in the configStatic assets fail too, not just pages
Branded page from a CDN or WAFA managed rule fired on this requestAn identifier on the page, and the same URL works with a different payload
The site's own themeThe application decidedThe rest of the site works while logged in as someone else

The middle row is the one that eats afternoons. A firewall rule matches on the content of the request, not on the URL, so the same address is fine in a browser and forbidden from a form submission, fine for you and forbidden for the client, fine all week and forbidden the moment somebody pastes an apostrophe into a text field. It presents as intermittent and it is perfectly deterministic; you are just varying the wrong input.

When you suspect it, vary one thing at a time: same URL with and without a body, same request with a browser user agent and with curl's default, same request from your address and from a phone on mobile data. The variable that flips the result names the rule.

The trap that belongs to agencies specifically

You put a staging site behind HTTP basic auth. Correct decision, and the reason it is correct has nothing to do with secrecy: it is the only reliable way to keep a staging copy out of the index, because a crawler that receives 401 never sees the page at all, whereas a crawler that receives 200 and a noindex tag has already read everything.

Then you add the staging URL to your monitoring, because you want to know when the build breaks. The checker gets 401 forever. You either mute the site, and stop watching it, or you keep the alert and teach the whole team to ignore that one red line, which is the same thing with extra steps.

Both outcomes are bad and there is a third. A protected site is working when it answers 401. Configure the check to expect 401 rather than 200, and it stays useful: an unexpected 200 then means the protection came off, which is exactly the incident you should be woken up for, and an unexpected 500 means the build is broken behind the guard. Any checker that lets you name the expected code will do this. A checker that only understands "up" cannot express it, and that limitation is precisely how staging copies end up indexed.

The same reasoning applies to a maintenance window, where the correct answer is 503 and the correct alert is its absence after the window closes. The general form of the idea, that several perfectly normal codes are worth counting rather than alerting on, is in the nine codes that matter in production.

When to serve 404 instead

A 403 confirms that something exists at that address. On a login path, an admin directory, or a client area, that is information you are giving away for free to anyone probing.

Returning 404 for resources whose existence should not be confirmed is allowed by the specification and is the sane default for admin surfaces. The cost is that your own team will hit a 404 on a page they know exists and lose ten minutes. Write it in the handover notes, and accept the trade on anything that faces the open internet.

Share this post.
Stay up-to-date

Subscribe to our newsletter

Don't miss this

You might also like