TL;DR: Use 401 when the client still needs valid credentials. Use 403 when the server knows the client identity but refuses the requested action.
The Fastest Way To Decide
Ask two questions in order:
- does the server know who this client is
- if yes, is this identity allowed to do this
If the answer to the first question is no, return 401. If the answer to the first question is yes but the second is no, return 403.
The Core Difference
Despite the name, 401 Unauthorized is really about authentication. 403 Forbidden is about authorization.
- 401 Unauthorized: the request lacks valid credentials, or the credentials are expired or invalid
- 403 Forbidden: the identity is known, but access is still denied
When Each Applies
| Scenario | Correct Code |
|---|---|
No Authorization header sent | 401 |
| Invalid or expired token | 401 |
| Valid token, but user lacks permission | 403 |
| Admin-only resource accessed by regular user | 403 |
| Resource exists but is hidden from this user | 403 (or 404) |
| IP blocklist | 403 |
| Rate limit exceeded | 429 (not 403) |
The Header That Usually Gives 401 Away
A 401 response must include a WWW-Authenticate header that tells the client how to authenticate:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"
A 403 response does not carry an authentication challenge, because the problem is not “please authenticate.” It is “authentication did not unlock this action.”
403 vs 404 When You Want To Hide Existence
There’s a deliberate security pattern of returning 404 instead of 403 for resources that exist but the user shouldn’t know about. If your API returns 403 for /admin/users, you’ve confirmed to an attacker that the endpoint exists. Returning 404 reveals nothing.
This is called “security through obscurity” and is a valid defense-in-depth measure for sensitive resources. The tradeoff: it makes debugging harder for legitimate users.
Browser Behavior
Browsers handle 401 specially: they may show a native authentication dialog (for Basic auth challenges) or trigger your app’s login flow. A 403 gets no special browser treatment — it’s just an error response.
Common Mistakes
Returning 403 when credentials are missing — if the client never authenticated successfully, the server is not at the permission question yet.
Returning 401 for permission failures — telling a logged-in user to “authenticate again” when they simply lack access is misleading and makes client behavior worse.
Using 403 for rate limiting — rate limiting is 429 Too Many Requests, not an auth decision.
Forgetting to log real 403s — they are often valuable signals for permission bugs or access probing.
API Design Guidance
For REST APIs:
GET /api/profile → 401 (no token)
GET /api/profile → 200 (valid token, own profile)
GET /api/users/other-id → 403 (valid token, not your profile)
GET /api/admin/dashboard → 403 (valid token, not an admin)
For the 403 case where you want to hide resource existence:
GET /api/users/other-id → 404 (valid token, resource hidden)
Choose 403 when the user should know the resource exists but they can’t access it. Choose 404 when revealing the resource’s existence is itself a security concern.
FAQ
Why is 401 called “Unauthorized” if it is about authentication?
Historical naming accident. RFC 9110 section 15.5.2 defines that 401 means the request lacks valid authentication credentials. The name “Unauthorized” is misleading — it should have been called “Unauthenticated”. Authorization failures are correctly represented by 403.
Should I return 403 or 404 when a user tries to access a resource they should not know exists?
Return 404. Returning 403 confirms to an attacker that the resource exists. Returning 404 reveals nothing. This is a valid defense-in-depth measure for sensitive resources, though it makes debugging harder for legitimate users.
Does a 401 response require a WWW-Authenticate header?
Yes, per RFC 9110 section 15.5.2. A 401 response must include a WWW-Authenticate header that tells the client how to authenticate. Omitting it is a spec violation. A 403 does not include this header.
What should I return when a valid API key has insufficient permissions?
Return 403. The client is authenticated (you know who they are via the API key) but not authorized to perform the requested action. Reserve 401 for missing or invalid credentials.
What is the difference between 403 and 429?
403 Forbidden means the server refuses to authorize the request regardless of how many times it is retried. 429 Too Many Requests means the client has exceeded a rate limit and should retry after a delay. Never use 403 for rate limiting.