Most teams pick a session model based on what feels modern the week they start building. Opaque server sessions look old. JWTs look clean. Both choices work fine until the day you have to migrate the auth stack, at which point the session model stops being an implementation detail and becomes the thing that decides how painful the cutover will be.
I have watched the same pattern on consumer SaaS migrations more than once. The identity provider change is planned carefully. The password export is handled. The MFA factors get mapped. Then someone asks how current users stay logged in during the switch, and the room goes quiet for a few seconds. That question is the one that should have been asked first.
What each model actually commits you to
A classic server session is a random identifier stored in a cookie, with the real state living in Redis, Postgres, or whatever session store you already run. Revocation is a delete. Rotation is a write. Looking up who is currently logged in is a query. Migrating means deciding what to do with that store: move it, dual-write it, or accept that everyone gets logged out on cutover.
A JWT session puts most of that state in a signed token the client carries. The server verifies the signature and trusts the claims until expiry. That is convenient when you have many services and do not want a shared session store. It is less convenient when you need to invalidate a token early, change the signing key, or prove during an incident that a specific user was logged out at a specific time.
Neither model is wrong. The mistake is treating the choice as fashion. The right question is which properties you will need when the next migration arrives, because every auth stack gets one eventually. Providers raise prices. Compliance requirements change. Acquisition deals force consolidations. The session model you pick now is the one your future self has to migrate.
The migration tax on JWTs
JWT sessions look cheap until cutover week. Then the costs show up in order.
First is key rotation. If your access tokens are signed with a provider-managed key, and that provider is the one you are leaving, every outstanding token becomes a problem. You can keep verifying the old keys until the last token expires. You can force a mass logout. You can issue a short dual-verify window where both old and new signatures are accepted. All three are real options. None of them are free. The dual-verify window is usually the least bad for consumer products, but it requires careful clock handling and a hard end date. Leave that window open "just a bit longer" and you will still be verifying legacy tokens six months later.
Second is claim shape. JWT payloads accumulate fields. Roles. Tenant ids. Feature flags. A custom claim someone added for a one-off partner integration three years ago. The new system will not have the same claim names, and some of the claims will not even mean the same thing. If your API gateway reads org_id and the new issuer writes organizationId, you do not have a session problem, you have a silent authorization bug waiting for Friday afternoon traffic.
Third is revocation. Token blacklists get built during incidents and then forgotten. During a migration they reappear as a requirement. If you cannot revoke a compromised session in under a minute, your security team will block the cutover, and they will be right. Opaque sessions make this trivial. JWTs make you invent infrastructure you pretended you did not need.
I have rolled back a JWT cutover because the revocation path was "wait for expiry." That is not a path. That is hoping.
The migration tax on server sessions
Opaque sessions have their own bill. The store itself becomes a migration object. If sessions live in Redis with a specific key layout, and the new auth service expects a different layout, you need a translator or a dual-write period. Dual-write sounds neat in a design doc and feels messy in production, especially when session writes come from three code paths you forgot about.
Logout semantics also matter more than people admit. Some products treat logout as "delete this session." Others treat it as "delete all sessions for this user." Others treat it as "mark the user logged out of this device and leave the rest alone." If the old and new systems disagree, support will hear about it before engineering does. Write the logout contract down before you migrate. Do not discover it from ticket volume.
There is also the sticky problem of session affinity during a staged rollout. If half your traffic hits the old session store and half hits the new one, users bounce between logged-in and logged-out states depending on which pod answers. Sticky routing can paper over that for a while. It also hides the real dual-read bugs until you remove stickiness and watch the error rate climb. Prefer dual-read with a single write authority over sticky routing that never gets removed.
A practical decision rule
If you expect to change identity providers in the next two years, prefer opaque server sessions for the browser session, and keep JWTs for short-lived service-to-service tokens where revocation windows are measured in minutes. That split is boring and it migrates cleanly. Browser sessions can be rewritten or invalidated on cutover. Short-lived service tokens expire before they become a multi-day liability.
If you already run JWT browser sessions and a migration is coming, shrink the access token lifetime before you start the cutover. A fifteen-minute access token with a refresh token that lives in an opaque server store gives you most of the distributed-verify benefits without stranding users on unverifiable credentials for a day. Then migrate the refresh store the way you would migrate any other session store. The access tokens take care of themselves by expiring.
If you must keep long-lived JWTs for some reason, plan the dual-verify window as a first-class work item with an owner, a dashboard, and a kill date. Count outstanding tokens by issuer kid. Watch that count go to zero. Do not declare the migration done while the old kid still verifies anything.
What to write down before you pick
Three decisions belong in the design doc before anyone debates libraries.
How do you revoke a session in under a minute, for one user and for all users?
What happens to currently logged-in users at cutover: stay logged in, soft reauth on next request, or hard logout?
Which claims or session fields are load-bearing for authorization, and which are convenience fields that can be recomputed?
Teams that answer those three questions early tend to finish on schedule. Teams that skip them ship a beautiful new login page and then spend the next month chasing session ghosts across environments. The session model is not the exciting part of an auth migration. It is usually the part that decides whether the exciting part survives contact with production.