<!-- llms.txt: https://workos.com/llms.txt -->

# Session resilience

## Introduction

AuthKit keeps a session alive by exchanging its refresh token for a new access token whenever the access token expires, and the SDKs handle this for you. When a refresh fails, one distinction determines whether the session survives: a *transient* failure should be retried and the session kept, while a *terminal* failure means the user must sign in again.

This page covers refresh token rotation, the replay grace period, and how to tell transient and terminal failures apart so a momentary problem never forces an unnecessary sign-out.

## Refresh token rotation

When you exchange a refresh token using the [authenticate with refresh token](https://workos.com/docs/reference/authkit/authentication/refresh-token) endpoint, AuthKit returns a new access token *and* a new refresh token, and the refresh token you sent is retired. This rotation limits the useful lifetime of any single refresh token.

Because the token rotates on every use, always persist the newly returned refresh token and discard the old one. If you store the refresh token in a database or cache, write the new value before serving the next request. The WorkOS SDKs that manage sealed sessions do this for you.

## The replay grace period

Rotation creates a race: if the same refresh token is exchanged twice at nearly the same moment — for example, two concurrent requests both notice an expired access token, or a client retries after a slow network — the second exchange would otherwise fail because the token was already rotated.

To make this safe, AuthKit applies a short **grace period** after a refresh token is exchanged. Within that period, replaying the just-exchanged token returns the *same* rotated tokens as the first exchange rather than an error. This means concurrent refreshes and network retries converge on one result instead of tearing down the session.

The grace period is 30 seconds.

Two consequences worth designing around:

- Retrying a refresh with the same token inside the grace period is safe and idempotent — you get the same new tokens back.
- Once the grace period elapses, the old token is terminally invalid. A replay after that point returns an `invalid_grant` error and the session must re-authenticate.

## Terminal vs. transient failures

The most important distinction when handling a failed refresh is whether the failure is *terminal* or *transient*.

**Terminal** failures mean the session is genuinely over and the user must sign in again. These are returned as an [authentication error](https://workos.com/docs/reference/authkit/authentication-errors) with the OAuth `invalid_grant` code at HTTP 400 — for example, a revoked, expired, or already-consumed (past the grace period) refresh token.

**Transient** failures mean the request didn't complete but the session is still valid. Retrying the same refresh token is the correct response. These include:

- Network errors and request timeouts (for example, a dropped connection or a `The read operation timed out` error).
- `5xx` responses from a temporary server-side or upstream issue.
- `429 Too Many Requests`, which AuthKit returns for short-lived refresh contention. Back off briefly and retry the same refresh token.

Use this table as the decision rule when a refresh completes:

| Response | Classification | What to do |
| --- | --- | --- |
| `200 OK` with new tokens | Success | Persist the rotated refresh token and continue |
| `400` with OAuth `invalid_grant` | Terminal | Clear the session and redirect to sign-in |
| `408 Request Timeout` or a network/connection error | Transient | Keep the session; retry with backoff |
| `429 Too Many Requests` | Transient | Keep the session; back off and retry |
| `500`, `502`, `503`, `504` | Transient | Keep the session; retry with backoff |
| Any other unexpected error | Treat as terminal | Clear the session and redirect to sign-in |

> **Note:** Never destroy a session on a transient failure. Clear session state and
> redirect to sign-in only for a terminal `invalid_grant`. Treating a timeout
> or `5xx` as terminal turns a brief blip into an unnecessary sign-out.

## Handling a failed refresh

How much of this you handle yourself depends on which SDK you use. The framework integrations that manage the session cookie for you are **turn-key** — they refresh automatically, keep the session on a transient failure, and redirect to sign-in only on a terminal `invalid_grant`. The backend SDKs hand you a typed result and never clear the session on your behalf, so **you decide** what to do with each outcome.

| SDK | Refresh & session handling | What you write |
| --- | --- | --- |
| `authkit-nextjs`, `authkit-remix`, `authkit-react-router`, `authkit-sveltekit`, `authkit-astro`, `authkit-tanstack-start` | Turn-key. The middleware or loader refreshes automatically, keeps the session cookie on a transient failure, and redirects to sign-in only on a terminal `invalid_grant`. | Nothing — resilient handling is built in. Optionally hook `onSessionRefreshError` to customize. |
| `authkit-js` (browser) | Turn-key. Refreshes in the background, preserves stored session state on a transient failure, and clears it only on a terminal failure. | Nothing required. Optionally handle the `onRefreshFailure` callback. |
| `workos-node`, `workos-python`, `workos-ruby`, `workos-go`, `workos-ios` (Swift) | Bring-your-own handling. [`refresh()`](https://workos.com/docs/reference/authkit/session-helpers/refresh) (or its equivalent) returns a typed result and never clears the session for you. Transient transport errors are retried internally, then surfaced. | Inspect the result: redirect to sign-in on a terminal outcome; keep the session and retry on a transient one. |

> **Note:** Keeping the session on a transient failure was added in recent versions of
> `authkit-nextjs`, `authkit-remix`, `authkit-react-router`, and `authkit-js`.
> Earlier versions signed the user out on any refresh failure, so upgrade to the
> latest to get the turn-key behavior described above.

Under the hood, the turn-key integrations are built on these backend SDKs, so the same rules apply — they just apply the terminal-vs-transient decision for you. For example, the Node SDK's HTTP client automatically retries idempotent failures — request timeouts (normalized to `408`), `429`, and `500`/`502`/`503`/`504` — with exponential backoff before surfacing an error, so a brief refresh contention or upstream blip is retried transparently rather than bubbling up as a failed refresh.

When you handle the result yourself, the rule is the same one the integrations follow: redirect to sign-in on a terminal outcome (such as `invalid_grant`), and reserve clearing the session cookie for that case. If a transient failure surfaces after the SDK's internal retries, keep the existing session in place and try again on the next request rather than signing the user out.

### Examples

**Turn-key framework (`authkit-nextjs`).** The middleware refreshes the session, keeps the cookie on a transient failure, and redirects to sign-in only on a terminal `invalid_grant`. You don't write any refresh logic; the callback below is optional and only for observability.

```ts
// middleware.ts
import { authkitProxy } from '@workos-inc/authkit-nextjs';

export default authkitProxy({
  middlewareAuth: { enabled: true, unauthenticatedPaths: [] },
});
```

```ts
// Optionally observe refresh failures (e.g. for logging). You don't need to
// act on these — the session cookie is already preserved on transient failures.
import { authkit } from '@workos-inc/authkit-nextjs';

const { session } = await authkit(request, {
  onSessionRefreshError: ({ error, isTransient }) => {
    if (isTransient) {
      console.warn('Transient refresh failure; session preserved.', error);
    }
  },
});
```

**Backend SDK (`workos-node`).** `refresh()` returns a typed result and never clears the session for you. The Node SDK additionally retries transient transport failures (timeouts, `429`, `5xx`) internally before returning, and reports anything still transient with `retryable: true`.

```ts
const session = workos.userManagement.loadSealedSession({
  sessionData: cookies.get('wos-session'),
  cookiePassword: process.env.WORKOS_COOKIE_PASSWORD,
});

const result = await session.refresh();

if (result.authenticated) {
  // Persist the newly sealed session and continue.
  cookies.set('wos-session', result.sealedSession);
} else if (result.retryable) {
  // Transient: keep the existing session and retry on the next request.
  console.warn('Transient refresh failure; session preserved.', result.reason);
} else {
  // Terminal (e.g. invalid_grant): clear the session and re-authenticate.
  cookies.delete('wos-session');
  redirectToSignIn();
}
```

**Backend SDK where you own the retry (`workos-go`).** Most language SDKs give you the same typed result but do *not* retry for you, so you add the bounded retry and backoff yourself. `Refresh` reports a terminal failure as `refresh_token_revoked` and anything else (`429`, `5xx`, network) as `refresh_failed`.

```go
result, err := session.Refresh(ctx)
switch {
case result.Authenticated:
    // Persist result.SealedSession and continue.
    setSessionCookie(w, result.SealedSession)
case result.Reason == "refresh_token_revoked":
    // Terminal: clear the session and re-authenticate.
    clearSessionCookie(w)
    http.Redirect(w, r, signInURL, http.StatusFound)
default:
    // Transient (refresh_failed): keep the session and retry with your own
    // bounded backoff rather than signing the user out.
    log.Printf("transient refresh failure, session preserved: %v", err)
}
```

### Without an SDK

If you call the [authenticate with refresh token](https://workos.com/docs/reference/authkit/authentication/refresh-token) endpoint directly, replicate what the SDK does: on a transient failure (a timeout, `5xx`, or `429`), retry the same refresh token with a bounded number of attempts and exponential backoff, keep serving the current request with the existing (still-valid) session while a retry is pending, and only give up — and re-authenticate — on a terminal `invalid_grant` or once retries are exhausted.

## Staying available during a service disruption

Applying the transient-versus-terminal distinction is what keeps your app usable when a dependency degrades and refreshes start failing intermittently:

- Keep the existing session and access token in place until the access token actually expires.
- Retry failed refreshes with backoff instead of treating the first failure as a sign-out.
- Redirect to sign-in only on a terminal `invalid_grant`.

This keeps a service disruption from cascading into signing out users who are already authenticated.

## Configuring session lifetimes

Grace-period behavior is separate from the session lifetimes you control in the dashboard. For maximum session length, access token duration, and inactivity timeout, see [Configuring sessions](https://workos.com/docs/authkit/sessions#configuring-sessions).
