401 vs 403 — The HTTP Status Code Difference Every Developer Gets Wrong

Updated April 2026

Quick Answer 401 Unauthorized means the request has no valid credentials — the user is not authenticated. The server doesn't know who you are. 403 Forbidden means the server knows who you are but denies access — the user is authenticated but lacks permission. Returning the wrong code breaks OAuth refresh logic and monitoring.

401 Unauthorized means the user is not authenticated — they need to log in. 403 Forbidden means the user is authenticated but not allowed. They are not interchangeable and using the wrong one breaks clients, caches, and monitoring.

The one-line rule

CodeMeaningWho are you?
401UnauthorizedI don't know who you are. Log in.
403ForbiddenI know who you are. You can't do this.

401 — Unauthenticated

Return 401 when the request lacks valid credentials. The user has not logged in, the token is missing, expired, or invalid.

The HTTP spec requires a WWW-Authenticate header on 401 responses telling the client how to authenticate:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"
Content-Type: application/json

{"error": "authentication_required", "message": "No valid token provided"}

When to return 401:

403 — Forbidden

Return 403 when the user is authenticated but lacks permission. The server knows who they are and the answer is still no.

HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error": "insufficient_permissions", "message": "Admin role required"}

When to return 403:

The security case for 404 instead of 403

Returning 403 reveals that the resource exists. An attacker learns they found a valid endpoint — they just can't access it. Returning 404 for unauthorized private resources prevents enumeration. This is a deliberate design choice, not a bug.

# Revealing (403)
GET /admin/users/123 → 403 Forbidden  # Attacker knows the resource exists

# Non-revealing (404)
GET /admin/users/123 → 404 Not Found  # Attacker learns nothing

What breaks when you mix them up

Wrong codeWhat breaks
Returning 403 for missing tokenOAuth clients won't retry with fresh token — they treat 403 as a permanent permission error
Returning 401 for insufficient permissionsClients loop on auth refresh — they think re-authenticating will fix it
Returning 200 for auth errorsMonitoring misses failures, retry logic breaks, caches store error responses

Framework examples

Express

// 401 — missing or invalid token
app.use((req, res, next) => { if (!req.headers.authorization) { return res.status(401) .set('WWW-Authenticate', 'Bearer realm="api"') .json({ error: 'authentication_required' }) } next()
})

// 403 — valid token but wrong role
app.get('/admin', requireAuth, (req, res) => { if (req.user.role !== 'admin') { return res.status(403).json({ error: 'insufficient_permissions' }) } // ...
})

FastAPI

from fastapi import HTTPException, status

# 401
raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"},
)

# 403
raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions",
)
Scan your API headers → HeadersFixer