I’ve been learning more about authentication and session management recently, and one concept that really changed the way I think about backend authentication is the separation between access tokens and refresh tokens.
At first, authentication can seem fairly straightforward:
Login → Get Token → Send Token with Requests
But once you start thinking about token expiration, security, revocation, multiple devices, and user experience, things become much more interesting.

Here’s the mental model that has started to make the most sense to me.
The problem with long-lived access tokens
Imagine giving a user an access token that stays valid for 30 days.
From a user-experience perspective, that’s convenient. The user doesn’t have to log in again for a long time.
But there’s an obvious security trade-off.
If that token gets stolen, it could potentially be used for the remainder of those 30 days.
And when the token is completely stateless, the server doesn’t necessarily have a straightforward way to revoke that individual token.
This is one reason access tokens are often kept short-lived.
For example:
Access Token
↓
Valid for 15 minutes
If the token is compromised, the window in which it can be used is much smaller.
But this creates another problem.
Nobody wants to enter their username and password every 15 minutes.
That’s where refresh tokens become useful.
Access tokens and refresh tokens have different jobs
The simplest way I think about it now is:
Access tokens are for accessing resources. Refresh tokens are for maintaining the session.
They solve different problems.
Aspect | Access Token | Refresh Token
----------------|---------------------------|---------------------------
Purpose | Access protected APIs | Obtain a new access token
Lifetime | Short | Longer
Usage | Frequent | Occasional
Server State | Generally stateless | Stateful session
Revocation | More difficult | Easier
Instead of having one long-lived credential responsible for everything, we separate those responsibilities.
LOGIN
│
┌─────────┴─────────┐
▼ ▼
Access Token Refresh Token
15 minutes 1 day+
│ │
▼ ▼
API requests Session record
│
▼
Database
That separation is what makes the overall design much more interesting.
The refresh token is what keeps the session alive
The refresh token isn’t something the client needs to send with every API request.
The client normally uses the access token when communicating with protected APIs.
The refresh token is kept for the specific situation where a new access token is needed.
For example, after login, the client might receive:
{
"access_token": "...",
"access_token_expires_at": "...",
"refresh_token": "...",
"refresh_token_expires_at": "..."
}
The client keeps the refresh token available and uses the access token for normal API requests.
When the access token is approaching expiration, the client can use the refresh token to obtain another access token.
This can happen proactively.
For example:
10:00
│
├── Login
│
└── Access token expires at 10:15
│
▼
10:14
│
▼
Refresh access token
│
▼
New token expires at 10:29
The client doesn’t necessarily have to wait until the access token has already expired.
Another approach is to let the access token expire, receive a 401 Unauthorized, and then use the refresh token to obtain a new access token before retrying the original request.
So the important idea isn’t necessarily “refresh exactly before expiration.”
It’s:
The client needs a strategy for obtaining a fresh access token when the current one is no longer usable.
What does the session add?
This is probably the biggest thing I took away from understanding this architecture.
We often hear that JWTs or other tokens are “stateless.”
That’s useful because the server doesn’t need to query a database for every API request just to determine whether the token is valid.
But statelessness also means giving up some server-side control.
A refresh-token session gives us that control back where it matters.
A session might contain information such as:
sessions
--------------------------------
id
username
refresh_token
user_agent
client_ip
is_blocked
expires_at
--------------------------------
Now the server has a record representing the user’s long-lived session.
If a refresh token is compromised, for example, the corresponding session can be blocked:
is_blocked = true
From that point onward, attempts to use that refresh token can be rejected.
This is something that’s much harder to achieve with a purely stateless, long-lived access token.
What actually happens during a refresh?
The refresh operation is more than simply:
Refresh Token → Valid → New Access Token
There can be several checks involved.
Conceptually:
Refresh Token
│
▼
Verify Token
│
▼
Find Session
│
▼
Check Session
│
├── Does it exist?
├── Is it blocked?
├── Does the user match?
├── Does the stored token match?
└── Has the session expired?
│
▼
Create New Access Token
I find this particularly interesting because the refresh token and the session record work together.
The token provides the credential, while the session gives the server control over that credential’s long-term validity.
Why keep session state?
This is where the trade-off between statelessness and statefulness becomes clearer to me.
For normal API requests:
Access Token
↓
Fast, stateless API authorization
For maintaining a long-lived login:
Refresh Token
↓
Stateful session
↓
Database
↓
Revocation / expiration / session control
We’re not necessarily choosing between a completely stateless or completely stateful authentication system.
We’re using each approach where it makes sense.
The access token can remain lightweight and short-lived, while the refresh-token session provides the server-side control needed for a longer-lived login.
Multiple devices make the idea even more useful
Another thing I like about session-based authentication is that it naturally maps to multiple devices.
A user might have:
Laptop → Session A
Phone → Session B
Tablet → Session C
Each login can create a separate session.
If we’re storing information such as the user agent and client IP, we can potentially build features around active sessions:
Active Sessions
Chrome / Windows
Last active: ...
Safari / iPhone
Last active: ...
Firefox / Linux
Last active: ...
And the user could revoke a particular session.
That’s a much richer model than simply thinking:
User → Token
Instead:
User
├── Session A
├── Session B
└── Session C
Each session can have its own lifecycle.
A subtle point: refresh token rotation
There’s also another concept that becomes interesting once you start thinking about refresh tokens: refresh token rotation.
One approach is to keep using the same refresh token until it expires or is revoked:
Refresh Token A
│
├──→ Access Token B
│
├──→ Access Token C
│
└──→ Access Token D
Another approach is to issue a new refresh token whenever the old one is used:
Refresh A
│
▼
Access B + Refresh B
│
▼
Refresh A becomes invalid
The second approach can provide additional protection against certain types of refresh-token theft.
It’s another reminder that authentication isn’t just about generating a token. It’s about deciding how long credentials should live, how they can be invalidated, and how the server maintains control over long-lived sessions.
One thing I’m now more conscious of
Where we store refresh tokens matters too.
If a database contains usable refresh tokens in plaintext, a database compromise could potentially expose active sessions.
One approach worth considering is storing a hash of the refresh token instead:
Client
│
│ actual refresh token
▼
Server
│
│ hash(token)
▼
Database
│
└── stored hash
Then the server can hash the incoming token and compare it against the stored value.
The broader lesson is that long-lived credentials deserve more protection than short-lived credentials, because they have a much larger window of usefulness.
The mental model I’m taking away
I used to think about authentication mostly in terms of:
Login → Token → API
Now I find this model much more useful:
LOGIN
│
┌────────┴────────┐
▼ ▼
Access Token Refresh Token
Short-lived Long-lived
Stateless Stateful
│ │
▼ ▼
API requests Session Database
│
▼
Revocation / Expiration
│
▼
New Access Token
The key idea is simple:
Keep access tokens short-lived for security, and use a stateful refresh-token session to provide a smooth user experience and server-side control.
Understanding this separation made authentication feel much less like “just add JWT” and more like an actual session-management problem.
There are many more details to explore when building this for production, but this distinction between access, refresh, and session state is one of those backend concepts that I think is worth having a solid mental model for.
What authentication concept took you a while to really understand?