401 Unauthorized is one of the worst-named status codes in HTTP. It does not mean you lack permission — that is 403. It means the server could not work out who you are at all, and it is inviting you to prove it.
That distinction decides where you look. A 401 is an authentication problem: a missing, expired, malformed or rejected credential. A 403 is an authorization problem: your identity was established and it simply is not allowed. Fixing a 401 means fixing how credentials are sent; fixing a 403 means changing a rule.
This guide covers the practical causes, the header most people forget is mandatory, and one proxy behaviour that silently breaks authentication on perfectly correct code.
401 vs 403 vs the others
These get used interchangeably in real APIs, which is exactly why debugging them is confusing. Here is what each one is supposed to mean.
| Code | Actual meaning | Can the caller fix it? |
|---|---|---|
| 401 Unauthorized | Not authenticated — identity unknown or rejected | Yes — send valid credentials |
| 403 Forbidden | Authenticated, but not permitted | No — the owner must change a rule |
| 407 Proxy Authentication Required | The proxy in between wants credentials | Yes — authenticate to the proxy |
| 419 / 440 (non-standard) | Session or CSRF token expired | Yes — refresh the page or token |
The header everyone forgets
The HTTP specification is explicit: a 401 response must include a WWW-Authenticate header telling the client how to authenticate. A 401 without it is technically an invalid response.
In practice a lot of APIs return a bare 401 with a JSON body and no such header. Browsers and HTTP clients rely on it to know what to do next — it is the header that makes a browser show the built-in username and password box for basic auth.
If you are building an API, send it: "WWW-Authenticate: Bearer" for token auth, or "WWW-Authenticate: Basic realm=..." for basic auth. If you are debugging someone else's API, its absence tells you the 401 is hand-rolled rather than coming from a standard auth layer, which is a useful clue about where to look.
Common causes, most frequent first
- The token expired. Access tokens are usually short-lived by design; the fix is refreshing them, not extending their lifetime.
- Wrong or missing API key. Check you are sending the key for the right environment — staging keys against production endpoints produce exactly this.
- Malformed Authorization header. "Bearer" with the wrong capitalisation, a missing space, or a stray newline from copy-paste all fail silently.
- Basic auth misconfigured on the server. A wrong path to .htpasswd, or a password file written with the wrong hashing format.
- The session expired. On a normal website this is the ordinary "you have been logged out" case, and it is not a bug.
- Clock skew with JWTs. If the server clock differs by more than the token tolerance, valid tokens are rejected as not-yet-valid or expired. Check the time on both machines.
- The Authorization header was stripped in transit — covered in the next section, because it is the one that wastes the most time.
The proxy that eats your Authorization header
This one deserves its own section because the code is correct, the credentials are correct, and it still returns 401.
Some reverse proxy and CGI setups drop the Authorization header before it reaches the application. Apache with mod_cgi and PHP-FPM is the classic case: unless CGIPassAuth is enabled or the header is explicitly forwarded, PHP never sees it and every authenticated request looks anonymous.
The symptom is distinctive: the same request works when sent directly to the application port, but fails through the public URL. If you can reproduce that difference, the proxy layer is the culprit, not your auth code.
How to confirm: log the raw headers your application actually receives. If Authorization is absent there while your client is definitely sending it, something between the two removed it. In Apache add "CGIPassAuth On"; in Nginx make sure your proxy_pass block is not filtering it; on managed hosting, ask support whether the header is passed through.
Check in this order
- Confirm the credential is actually being sent. Log the request headers on the client side, or use "curl -v" — the -v flag prints what was sent.
- Test the same credential with curl directly against the endpoint. If curl works and your application does not, the problem is in your client, not the server.
- Check the token is not expired. Decode a JWT at any decoder and read the exp claim; do not assume, verify.
- Compare server clocks if JWTs are involved. Even a couple of minutes of skew rejects valid tokens.
- Log the headers the server actually receives. This is where stripped Authorization headers show up.
- Check whether the endpoint expects a different scheme. Some APIs want "Bearer <token>", others want an "X-API-Key" header, others a query parameter. Sending the right value the wrong way still returns 401.
- Read the response body. Many APIs return a specific error code inside the JSON that distinguishes expired from invalid from revoked — three different problems with three different fixes.
If you are protecting your own site
When you are the one issuing the 401, a few choices make it much easier for everyone to work with.
Always send WWW-Authenticate, as above. It is required, and it is what tells clients how to proceed.
Distinguish expired from invalid in the response body. "Token expired" tells the client to refresh; "token invalid" tells it to re-authenticate from scratch. Returning the same opaque message for both forces the client to guess.
Do not use 401 to hide the existence of a resource. If you want to conceal whether something exists, return 404 — that is the established pattern. A 401 confirms there is something there worth authenticating for.
Rate-limit failed authentication attempts. Endpoints that return 401 are exactly what credential-stuffing tools hammer, and an unlimited login endpoint is an open invitation.
Keep basic auth behind HTTPS only. Basic auth sends credentials in a trivially decodable form; over plain HTTP they are effectively in the clear.
Need to see the headers your server actually receives?
Cloud NVMe VPS with full root access — real logs, your own proxy and auth configuration. From ฿150/month.
Frequently Asked Questions
What is the difference between 401 and 403?
401 means the server does not know who you are — authentication failed or was never provided, and sending valid credentials could fix it. 403 means you have been identified and still are not allowed, so no credential change will help. HTTP named them backwards: 401 is really "unauthenticated" and 403 is "unauthorized".
My API key is correct but I still get 401. Why?
Check three things in order: that the header is actually reaching the server (log the received headers), that you are using the scheme the API expects (Bearer vs X-API-Key vs query parameter), and that the key belongs to the environment you are calling. A staging key against a production endpoint returns 401 with no other clue.
Why does authentication work locally but fail in production?
Very often a reverse proxy or CGI layer is stripping the Authorization header before your application sees it. Apache with PHP is the classic case and needs CGIPassAuth enabled. Log the raw headers your app receives in both environments and compare — that comparison usually finds it in a minute.
Does a 401 hurt SEO?
Only if it appears on pages that should be public. Googlebot cannot authenticate, so any page behind a 401 will not be indexed — which is correct behaviour for genuinely private areas. It becomes a problem when an overly broad auth rule accidentally covers public pages; use URL Inspection in Search Console to see what Googlebot actually receives.
GUIDES
Related articles
Keep reading on similar topics
403 Forbidden — What It Means and How to Fix It
A 403 is not a broken server. It is a server that understood exactly what you asked for and decided you are not allowed to have it. This guide covers how 403 differs from 401, the causes in order of likelihood, and the cases where a 403 is the server doing its job properly.
Read more429 Too Many Requests — Causes and How to Fix It
The first question is not how to fix it — it is who is doing the limiting. Your own server, the CDN in front of it, and the API you are calling all return the same 429, and the fix is different for each.
Read more500 Internal Server Error — Causes and How to Fix It
A 500 is the least informative error on the web: it means "something went wrong and I am not telling you what". The good news is that the server almost always wrote the real reason to a log file. This guide shows you where that log lives and how to work through the causes in the order that finds the culprit fastest.
Read more