Like everything, there’s no silver bullet (JWT vs random string token) - business requirements dictate which makes sense.
Turns out that just deleting JWTs from the client and letting them expire naturally (10 mins) has always been good enough so far.
making the expiration short works to some degree, but then you'd be forced to constantly update the token.
As for deleting the jwt from the client - that's not trustworthy enough, since the client controls themselves, you can't be sure that a deleted jwt is really deleted!
Usually I work with web applications which have some server side state but where bits are made stateless where sensible. So for example, user records exist but we can save a db/redis call per request with stateless sessions.
To address your points:
It’s fairly easy to do jwt refresh (usually client driven with a timer) which lets you keep individual jwt lifetimes short. The refresh endpoint can sometimes be stateless.
This means that an inactive timeout happens by default but a hard limit on maximum session lifetime (e.g. can refresh for up to 24h say) needs to be implemented at the application level (you can also do this statelessly).
There’s design impact using stateless sessions but it’s usually positive in my experience. When you ditch state in server side sessions, you have to figure out where to put it. Take say, “last search filter” or something. Traditionally you might have stored that in server side session storage, deserializing on every request. Using JWTs you will tend to either offload that to client JS or expose it as a “real” resource tied to the user record.
If you use public key signed sessions you can also decouple the “auth” part of your app from the jwt consumers in a fairly principled way (e.g. in microservices arch most services then don’t have the ability to issue sessions, in fact your auth service can be in say its own aws account etc).
It’s not quite true that you negate all the benefit of using jwts with a stateful “revocation” cache.
The key difference is that it is likely that the number of sessions actually revoked in the last 10 minutes tends to be rather small compared to total concurrent active sessions. This is faster and cheaper to store and search.
But 90% of the time you don’t actually need to build a revocation cache.
Usually there are two reasons to “revoke” a jwt in a webapp, the first would be to do a “hard” user initiated logout. This means that the user is explicitly asking for their session to be invalidated on the server side. The second would be to allow an admin to terminate a specific user session.
Often when an admin locks a user out it’s better to actually implement that at the level of the user record rather than mucking about with specific sessions. Permission revocation also happens at the level of the user record.
In the logout case, the trust issue you mention is not an issue because the client itself is requesting invalidation.
Generally the security difference between having the client simply delete its copy of the jwt and actually doing hard invalidation on the server is fairly minimal. May be a requirement for some projects though.
I think a lot of these “to jwt or not to jwt” posts suffer from all-or-nothing thinking, real apps can pick and choose along a spectrum of options.