HTTP 401 vs 403 — Unauthorized vs Forbidden Status Code Comparison
Confused about http 401 vs 403? Learn the exact difference between Unauthorized and Forbidden, when each is returned, and how Express, Stripe, and GitHub APIs use them.
Quick Cheat Sheet
| Question | 401 Unauthorized | 403 Forbidden |
|---|---|---|
| Who is the caller? | Unknown — no/invalid credentials | Known — credentials are valid |
| What's missing? | Authentication | Authorization (permission) |
| Should client retry with new creds? | Yes | No — re-auth won't help |
| Required response header | WWW-Authenticate |
None |
The Core Distinction
Despite the name, 401 Unauthorized is really about authentication, not authorization. RFC 9110 § 15.5.2 says 401 means the request "lacks valid authentication credentials." In contrast, 403 Forbidden (RFC 9110 § 15.5.4) means the server understood who you are but refuses to fulfill the request anyway.
A useful mnemonic: 401 = "I don't know you", 403 = "I know you, and no."
When Each Is Returned
Return 401 when:
- The
Authorizationheader is missing entirely - The bearer token is expired, malformed, or not signed by your issuer
- A session cookie is invalid or has been revoked
Return 403 when:
- The token is valid but the user lacks the required role/scope
- The user owns a resource but the action is disabled by policy
- The request originates from a blocked IP, country, or rate-limit tier
- A WAF rule rejects the payload (Cloudflare often returns 403)
Real-World API Examples
- GitHub API returns
401 Bad credentialsfor an invalid PAT, but403 Forbiddenwhen you hit the abuse-detection or secondary rate limits. - Stripe API uses
401 Invalid API Keyfor malformed/revoked keys, and403is rare — Stripe prefers402 Payment Requiredor400for permission-style failures. - AWS S3 returns
403 Forbiddenfor both expired pre-signed URLs and bucket-policy denials, which is a frequent source of confusion.
Common Implementation Mistake
Many APIs return 403 when a token is missing simply because they want to hide the existence of an authentication system. This violates RFC 9110 — if no credentials are presented, the correct status is 401 with a WWW-Authenticate header. If you want to obscure resource existence, return 404 instead.
Real-World Use Case
In an Express.js middleware chain, the auth middleware should return 401 when JWT verification throws (no/invalid token), then a separate authorization middleware should return 403 when the verified user's role doesn't match the required scope. CDNs and WAFs (Cloudflare, AWS WAF) typically use 403 for IP/geo blocks because the caller is identified but denied.
Look Up Any Status Code
Related Comparisons
HTTP 400 vs 422 — Bad Request vs Unprocessable Entity Comparison
http 400 vs 422 explained: when to return Bad Request for malformed syntax versus Unprocessable Entity for valid syntax that fails business validation. Includes API examples.
HTTP 404 vs 410 — Not Found vs Gone Status Code Comparison
http 404 vs 410: Not Found means the resource may exist later, while Gone signals permanent deletion. Learn how Google indexing, CDN caching, and SEO are affected.
HTTP 511 vs 401 — Network Authentication Required vs Unauthorized Comparison
http 511 vs 401: 511 is for captive portals (Wi-Fi login pages) and is intercepted by network-level proxies, 401 is application-level auth. Critical distinction for HTTPS detection.
HTTP 405 vs 501 — Method Not Allowed vs Not Implemented Comparison
http 405 vs 501: 405 means this method isn't allowed on this resource, 501 means the server doesn't implement the method at all. With Allow header guidance.