Tutorial
How to store authentication tokens with Redis
March 25, 20269 minute read
TL;DR:Store opaque auth tokens in Redis withSET ... EX, validate on each request withGET, rotate on refresh, and delete on logout. TTL handles cleanup automatically.
Note: This tutorial uses the code from the following git repository:
#What you'll learn
- How to store auth tokens in Redis with automatic TTL-based expiry
- How to validate a bearer token on every request with a single
GET - How to rotate a token on refresh
- How to revoke a token instantly on logout
- How to protect API routes with token middleware
#What you'll build
You'll build a Bun and Express API with auth routes and a protected todo CRUD. The auth routes are:
POST /api/auth/register— create an accountPOST /api/auth/login— log in and receive a bearer tokenPOST /api/auth/logout— revoke the tokenPOST /api/auth/refresh— rotate the tokenGET /api/auth/me— get current user info
The todo routes (
GET, POST, PATCH, DELETE under /api/todos) all require a valid Authorization: Bearer <token> header.A login response looks like this:
#What is token-based authentication?
Token-based authentication is a pattern where the server issues a token after a user logs in. The client sends that token as a bearer token with every subsequent request. The server validates the token, identifies the user, and decides whether to allow the request.
The server needs a fast way to validate, rotate, and revoke tokens. That is where Redis fits—it stores the live token state so every request gets a sub-millisecond lookup, and TTL handles cleanup automatically.
#Why use Redis for authentication token storage?
Redis is a strong fit when you need fast token lookup and instant revocation. A database can store token state, but Redis gives you low-latency reads, built-in TTL-based expiration, and single-command invalidation with
DEL.This app uses Redis as a token registry, not as the identity provider. The API decides who can log in, but Redis stores the live token state your app checks on every request.
#Prerequisites
- Bun
- Docker
- A Redis instance, either local or Redis Cloud
- Basic familiarity with TypeScript and HTTP headers
#Step 1. Clone the repo
#Step 2. Configure environment variables
Copy the sample file:
If you run Redis locally, the default
REDIS_URL points at redis://localhost:6379. If you use Redis Cloud, replace that value with your cloud connection string. The file also documents optional variables like TOKEN_TTL (default 3600 seconds).#Step 3. Run the app with Docker
#Step 4. Run the tests
The test suite includes unit tests for validation schemas and integration tests that cover the full auth lifecycle—register, login, access a protected route, refresh, and logout.
#How does the app store tokens in Redis?
The app uses two key prefixes:
| Key | Value | TTL | Redis command |
|---|---|---|---|
tokens:{uuid} | The username that owns the token | TOKEN_TTL (default 3600s) | SET ... EX |
users:{username} | JSON string with username, password hash, and created date | None | SET ... NX |
Token validation is a single
GET:- Look up
tokens:{token}in Redis. - If the key exists, the value is the username—the token is valid.
- If the key is missing, the token has either expired (TTL removed it) or been deleted (logout removed it). Return
401.
This is the core Redis pattern. Every token check is one
GET with O(1) complexity, and Redis removes expired tokens automatically.#How does registration work?
POST /api/auth/register creates a new user account. The app hashes the password with Bun.password.hash() and stores the user data in Redis with SET ... NX.The
NX flag tells Redis to set the key only if it does not already exist. This prevents duplicate accounts atomically—no separate "check then insert" step that could race.Example request:
Example response:
Under the hood, Redis stores:
#How does login work?
POST /api/auth/login accepts a username and password. The app reads the user record from Redis with GET, verifies the password hash with Bun.password.verify(), and generates a UUID token. It stores the token in Redis with SET ... EX so the token expires automatically after TOKEN_TTL seconds.Example request:
Example response:
Under the hood, Redis stores:
The client includes this token in all subsequent requests as
Authorization: Bearer d6ad7dae-....#How does request validation work?
Every protected route runs through the
requireAuth middleware. The middleware:- Extracts the bearer token from the
Authorizationheader. - Calls
GET tokens:{token}in Redis. - If the key exists, the value is the authenticated username. The request continues.
- If the key is missing (expired or deleted), the middleware returns
401.
Example request to a protected route:
The lookup is a single
GET—no joins, no secondary index, no parsing. Redis returns the username or nil.#How does token refresh work?
POST /api/auth/refresh rotates the token. The middleware validates the current token, then the handler deletes the old key and creates a new one with a fresh TTL.The steps:
GET tokens:{old-token}— retrieve the username.DEL tokens:{old-token}— remove the old token.SET tokens:{new-token} username EX 3600— store the new token with a fresh TTL.
Example request:
Example response:
After the refresh, the old token returns
401 and the new token is the only valid one.#How does logout work?
POST /api/auth/logout deletes the token from Redis with a single DEL. The next request using that token gets nil from GET and the middleware returns 401.No revocation markers, no cleanup jobs.
DEL removes the key immediately and the token is invalid from that moment forward.#How it works
The full token lifecycle breaks into two sequences:
Redis stores the token records as strings with TTL and the user records as JSON strings. TTL handles cleanup automatically—even if a client never logs out, the token expires and Redis removes the key.
#FAQ
#Should I store JWTs in Redis?
If you need instant revocation or a server-side view of active sessions, store token state in Redis. This tutorial uses opaque tokens (random UUIDs), which is the simplest pattern for lookup and revocation. If you use signed JWTs, Redis still helps when you need a blocklist, a token registry, or a refresh-token store.
#How do I revoke auth tokens with Redis?
Delete the token key with
DEL. The next GET for that token returns nil, and the middleware rejects the request immediately. No expiry window, no eventual consistency—revocation takes effect on the next request.#How long should auth tokens live in Redis?
It depends on the risk profile. This tutorial defaults to 3600 seconds (1 hour). Shorter TTLs reduce the window if a token leaks, but force more frequent refreshes. Longer TTLs improve UX but keep tokens valid longer after a credential change. Choose based on your threat model and adjust
TOKEN_TTL in the .env file.#When should I use Redis sessions vs token storage?
Use Redis sessions when your app owns a single server-side session ID and the browser simply presents that ID (typically via a cookie). Use token storage when clients present bearer tokens and you need refresh, rotation, or per-token invalidation—common in APIs, mobile apps, and SPAs.
#Troubleshooting
#The app starts but returns a Redis error
Check that
REDIS_URL in your .env file points to a running Redis instance. If you use Docker, verify the container is healthy:#Registration or login returns an unexpected error
Make sure the app container has finished starting. Check the server logs:
#Docker Compose fails to start
Make sure Docker is running and that port 8080 is not already in use by another service.
#Next steps
- Read Mobile banking session management to compare token storage with session storage.
- Read API gateway caching to see how Redis helps when auth data sits at the gateway.
- Read Rate limiting to see another TTL-heavy Redis control pattern.
- Read Slackbot distributed locking for another example of fast, server-side state control with Redis.
