← Back to blog

Database-backed sessions vs. JWTs: when each one wins


Database-backed sessions and JSON Web Tokens solve different versions of the post-login problem. A database session gives the client an opaque secret and checks current server-side state on each request. A self-contained JWT gives the client signed claims that a verifier can validate without asking the issuer for the same state. The practical choice is therefore not whether JWTs are modern or whether a database lookup is expensive. It is where session authority must live and how quickly a change to that authority must take effect.

A JWT is a format, not a session architecture

RFC 7519 defines JWT as a compact representation of claims. Those claims can be signed, authenticated with a message authentication code, encrypted, or nested. The format does not require an application to be stateless. A JWT verifier can still query a user record, check a revocation store, or load current permissions. Once it does, the system has combined a signed token with server-side state rather than eliminated that state.

The useful comparison is between an opaque database session and a self-contained signed JWT used as the complete authority for a request. The opaque token contains no user claims. Its value selects a session record. The self-contained JWT carries claims such as issuer, subject, audience, and expiry, and the receiving service decides whether to accept those claims after validating the token. Signed JWT payloads are integrity-protected, not confidential by default, so their contents should be treated as visible to the token holder unless encryption is explicitly used.

Database sessions keep validity current

A database-backed flow generates a random session token, stores a protected representation with the session record, and returns the raw token to the client. Verification resolves the presented token against that record and checks its current expiry and revocation state. The lookup makes the stored record authoritative at request time rather than at token-issuance time.

That property makes targeted control straightforward. Signing out can revoke one session. A manage-devices screen can list separate sessions and revoke a selected one. A password reset or disabled account can invalidate every active session. Absolute expiry and idle expiry can be evaluated from current timestamps, with successful activity extending only the idle deadline. The next verification observes each change without waiting for a client credential to reach its original expiry.

The session record should not become a substitute for authorization data. Current organisation membership, role assignments, resource ownership, and account restrictions can change independently of the login event. Applications that need those values to be current should load them from their authoritative records even when the session itself is valid.

JWTs move verification across a boundary

Self-contained JWTs are strongest when the issuer and verifier are separate systems. An authorization server can issue a short-lived access token to a client, and multiple resource servers can validate its signature, issuer, audience, and expiry using the issuer's published keys. RFC 9068 standardizes this model for OAuth 2.0 JWT access tokens. A resource server does not need access to the issuer's session database to accept a correctly scoped token.

That separation matters for APIs spanning independent services, security domains, or infrastructure owned by different teams. Verification can continue while the issuer is unavailable, provided the verifier still has the required keys and policy. The token also carries the audience and scope needed for the specific API boundary. These are stronger reasons to use JWTs than avoiding one lookup inside a single application backend.

Revocation and claim freshness set the limit

A valid signature proves that the issuer created the JWT and that the protected contents have not changed. It does not prove that the claims are still current. A role can be removed, an account can be disabled, or a device can be reported stolen while a previously issued token remains inside its validity window. If the verifier relies only on the token, those changes take effect when the token expires.

Short access-token lifetimes bound that delay. Immediate invalidation requires another mechanism, such as token introspection, a revocation or deny list keyed by token ID, a user or session version check, or coordinated key invalidation. RFC 7009 also notes that revoking a refresh token does not necessarily invalidate an access token immediately. Each added check is legitimate, but it changes the latency, availability, and state model that made self-contained verification attractive.

Refresh tokens do not make the design inconsistent. They separate a short-lived credential presented frequently from a longer-lived credential exchanged at the issuer. The issuer can rotate and revoke refresh tokens while resource servers validate access tokens locally. The resulting revocation window is a deliberate property of the system, not a hidden promise of instant logout.

The latency trade-off includes failure behavior

Database-session verification adds a storage operation to protected requests. That lookup needs an index, bounded query time, and enough database capacity for the authentication traffic. Caching can reduce repeated reads, but the cache lifetime also becomes the maximum delay before revocation or expiry is observed unless the application implements explicit invalidation.

Local JWT verification replaces that read with cryptographic work and key access. It also changes the outage mode: a database-session request fails when the session store is unavailable, while a resource server with cached verification keys may continue accepting unexpired JWTs during an issuer outage. The other side of that availability is reduced central control until the token expires or the verifier receives updated revocation information.

Own Auth keeps application sessions in current state

Own Auth uses opaque, database-backed sessions for signed-in application users. It returns the raw session token to the application and stores its protected hash. auth.getCurrentSession resolves that token and returns null when the session is unknown, expired, revoked, or belongs to a disabled user. A successful check updates the activity timestamp and idle deadline without moving the absolute deadline.

The same model supports auth.listSessions, auth.revokeSession, auth.revokeAllSessions, and auth.signOut. Own Auth marks revoked sessions instead of deleting their records, preserving the lifecycle data needed for account security history. This is the right boundary for an application that wants each protected request to observe current session state and wants one session, every session, or a disabled account to stop working at the next check.

A system can use both models without conflict. Database sessions can maintain the user's application login while the backend issues narrowly scoped, short-lived JWT access tokens to independent services. Keep the database session when the application needs current lifecycle control. Use a self-contained JWT when a separate verifier needs portable claims and an explicit expiry window is acceptable. The deciding factor is the trust boundary, not the token's popularity.