If you work in cloud infrastructure or ad tech, you know the dream of stateless architecture. It is the holy grail of modern scaling. You spin up scalable microservices (+ 1 mln requests per second), you issue JSON Web Tokens (JWTs) for authentication, and you free your databases from the tyranny of checking session validity on every single API call. It is elegant, it is fast, and it is the standard for a reason.
But there is a specific nightmare scenario that haunts stateless architectures, one that we ran into recently at Appliscale while working with a major gaming client. It is the moment when the “stateless” strength becomes a critical weakness. The moment when you realize that once a token leaves your server, you have lost control of it.
We were tasked with building a system for a massive gaming platform with tens of millions of daily active users. This wasn’t just about keeping the servers running; we had to solve a regulatory compliance problem that carried massive financial penalties. The law required strict enforcement of playtime limits – for example, preventing minors from playing more than one hour a day or stopping gameplay during curfew hours.
This sounds like a simple business rule. But when you apply it to a distributed system serving millions of adversarial users – players who actively try to bypass rules to keep playing – it exposes the cracks in the standard JWT model. We found ourselves in a situation where the standard architectural patterns failed us, and we had to rethink our entire approach to session management.
This is the story of how we fell into the JWT Trap, the exploit that kept us up at night, and how we eventually solved the problem of JWT revocation by inverting the standard authentication flow.
Scaling Identity: Why We Chose Stateless JWTs for +10 Millions of Users
To understand why this was such a headache, you have to understand the environment. We weren’t building an internal enterprise tool where users are generally well-behaved employees. We were building for the internet – specifically, the gaming internet.
In this world, the users are adversarial. If there is a loophole, they will find it. If they can tweak a client-side script to get five more minutes of playtime, they will do it. If they can automate a bot to farm resources, they will.
The platform was massive. We are talking about tens of millions of players hitting the login services every day. To handle this load, the architecture was built on microservices communicating via stateless JWTs.
For those who might need a refresher, the beauty of the JWT is its self-containment. When a user logs in, the authentication service verifies their credentials and signs a token that says, essentially, “This is User 123, and this token is valid for 10 minutes.” The user’s client holds onto that token and sends it with every request. The game servers receive the request, check the cryptographic signature, see that the token hasn’t expired, and let the user play.
The game servers don’t need to call the database to check if the user is logged in. They trust the token. This is what allows the system to handle millions of requests per second without melting down the central database.
But then came the legal requirement: Hard Playtime Limits.
The regulators were clear. The rules were strict and dynamic. For example, preventing minors from playing more than one hour a day, or enforcing strict “allowed windows” where gameplay is only permitted between 6 PM and 7 PM.
If a player is a minor and they have played their allotted one hour, or if the clock strikes 7 PM, they must be stopped. While a small margin of a few minutes is usually acceptable, the goal is near-immediate enforcement. If our logs showed that users play for 75 minutes when the limit is 60, the client could face severe regulatory fines.
We looked at the requirements, looked at our shiny stateless architecture, and realized we had a conflict. How do you enforce a hard stop in a system designed specifically to not check the central state?
The “JWT Trap”: Why Stateless Tokens Resist Revocation
Our first instinct was the standard engineering approach. We thought, “Okay, we have a Compliance Service. It holds the complex rule engine—time limits, curfews, holiday variances. It tracks the user state. When they hit the limit, we will just revoke their session.”
It sounds reasonable. The user plays, the clock ticks, and when the Compliance Service sees they have hit the limit or exited the allowed time window, it triggers a “Stop” command.
But here is the problem that isn’t always obvious in the tutorials: You cannot easily revoke a stateless JWT.
Once the Authentication Service signs that token and hands it to the client, that token is valid until its expiration timestamp. If we issued a token valid for 10 minutes, that token is good for 10 minutes. Even if we ban the user in the database one second later, the game server – which is just validating the signature – will still accept that token for the remaining nine minutes and fifty-nine seconds.
We knew this, of course. We accepted that there would be a “bounded delay.” If a user hits their limit, they might get a few extra minutes of free play until the token expires. In many systems, this is an acceptable trade-off for scalability.
But then we started testing against adversarial behavior, and the bounded delay turned into an unbounded exploit.
The Infinite JWT Recreation Loop: Bypassing Playtime Limits
Here is exactly what went wrong. We designed a reactive system. This was partly due to the existing communication model: the Compliance Service tracked playtime by subscribing to asynchronous session events (login, refresh, logout, etc.) emitted by the Authentication Service.
This meant the Compliance system only knew a player had started a session after the fact. There was no API to synchronously check if a user “WILL BE” allowed to play before the JWT was already issued.
So, the user would log in, get a token, and start playing. The Compliance Service would run in the background, tallying up their minutes based on those events. When they hit the limit, the backend would send a signal to the client saying, “Time is up. Session terminated.”
In a cooperative environment, the game client would receive that message, display a “Time’s Up” popup, delete the local token, and boot the player back to the main menu.
But we couldn’t trust the client.
We realized that a sophisticated user – or a modified game client – could simply ignore that termination message. The backend says “Stop,” and the client puts its fingers in its ears and keeps sending the token.
“Fine,” we thought. “The token will expire in a few minutes anyway. Then they will be kicked out.”
And this is where the real exploit happened.
The Loophole: Requesting a Fresh JWT vs. Refreshing
While we could easily block a token refresh (extending the life of an existing session), we couldn’t easily block the creation of a new session.
A “refresh” says: “I am already here, let me stay longer.” A “login” (new session) says: “I am arriving now, let me in.”
In our reactive model, we relied on the client to stop playing. But an adversarial client could simply drop the current session and immediately request a brand new one substituting the old one.
Why couldn’t we just block the creation of all new sessions, after the previous session was revoked? Because at scale, you can’t assume a user is banned forever. The rules are dynamic. A user might be banned right now because it is 5:59 PM (outside the allowed window), but they might be allowed back in 1 minute when the 6 PM window opens. Or they might be banned for reaching a daily limit, which resets at midnight.
The Auth Service doesn’t know why a user is banned, nor when they will be unbanned. It just knows they are valid users. If we tried to replicate the complex compliance state (“Banned until X”) into the Auth layer to block logins, we would risk significant synchronization issues and duplicate logic.
So, the user would hit the limit. We would send the “Stop” signal. The adversarial client would ignore it. Then, instead of refreshing (which we blocked), the client would simply log in again and substitute the old token with the new one.
The Authentication Service would look at the credentials. “Is this a valid user? Yes. Is the password correct? Yes.” So, it would mint a new 10-minute token.
The user would take that new token and keep playing. Ten minutes later, the cycle would repeat. The Compliance Service would be screaming “This user is banned!”, sending termination signals into the void, while the Authentication Service happily handed out fresh keys to the castle every ten minutes.
| Scenario | The “Happy Path” Assumption | The Adversarial Reality |
|---|---|---|
| Session End | Server sends a “Logout” signal; Client deletes the token. | Client ignores the signal; keeps using the active, non-revocable JWT. |
| Token Expiry | Client tries to refresh the token. If not possible – stops the game | Scripted client detects expiry and auto-pings the login endpoint instantly. |
| Limit Hit | The user stops playing once the limit is reached. | The user clears local state or scripts the client to enter a Recreation Loop. |
| Audit Trail | Logs match the business rules exactly. | Regulatory Breach: Logs show usage far exceeding the legal limits. |
| Result | Compliance achieved. | Unbounded Playtime (The JWT Trap). |
We had created a recreation loop. The user was effectively banned, yet they could play forever. The bounded delay of 10 minutes wasn’t a cap; it was just the interval at which they refreshed their exploit.

Why Reactive Enforcement Fails with Stateless JWT Architecture
We spent some time trying to patch this with more reactive measures. We looked into “Blacklisting” tokens. This involves setting up a shared cache (like Redis) where we store the IDs of revoked tokens. Every time a game server receives a request, it would have to check the global cache to see if that specific token had been blacklisted.
But think about the scale. We are talking about millions of players sending constant requests. If every single request requires a lookup in a central Redis cache to check for blacklisting, we have just reintroduced the exact bottleneck we were trying to avoid by using JWTs in the first place. We would be turning our stateless architecture back into a stateful one, introducing latency and a single point of failure.
We also considered shortening the token lifetime to something ridiculous, like 30 seconds. That way, the “free time” a user gets is minimal. But this still doesn’t solve the fundamental problem: the recreation loop remains intact, it just triggers more frequently. Plus, it puts an immense strain on the authentication infrastructure. If millions of clients are refreshing their tokens every 30 seconds, your login service becomes a DDoS target for your own users.
We realized we were fighting a losing battle. We were trying to chase the user after they had already entered the building. As long as the Authentication Service was willing to issue a token (and we are talking about a service capable of handling around thousands of requests per minute at peak time), the user could find a way to use it.
The realization hit us during a whiteboard session: We didn’t need better revocation. We needed to stop issuing the tokens in the first place.
| Strategy | Mechanism | Impact on Scale | Why We Rejected It |
|---|---|---|---|
| Token Blacklisting | Store revoked IDs in a global cache (Redis). Check every request. | High. Turns stateless auth stateful; reintroduces DB bottlenecks. | Reintroduces the exact scalability issues JWTs were meant to solve. |
| Ultra-Short TTL | Set token expiry to < 30 seconds. | Critical. Massive CPU/Traffic load on the Auth Service. | Turns regular users into a self-inflicted DDoS; doesn’t stop the loop. |
| Client-Side Enforcement | Trust the game client to block the user. | None. | Trivial Bypass. Adversarial users can modify or bypass client code. |
| Post-Issuance Logic | Detect violation after login and attempt revocation. | Low. | Reactive failure. Fails because JWTs cannot be revoked effectively. |
| Pre-Issuance Check | Verify eligibility synchronously before signing the token. | Winner. Minimal impact (+50ms on login only). | The Solution. Stops the loop at the source by making issuance the policy boundary. |
The Architecture Shift: Making JWT Issuance the Policy Boundary
The solution wasn’t a new tool or a specific library. It was a fundamental change in the sequence of events. We moved from a Post-Issuance Enforcement model to a Pre-Issuance Enforcement model.
In the old world, our flow looked like this: The user asks for a token, the Auth Service verifies the password and issues the token. The user plays, and then later the Compliance Service checks if they should be playing and tries to stop them.
We flipped the script. We decided that the “Eligibility Check” – the question of whether a user is legally allowed to play right now – had to happen synchronously, before the token was ever signed.
The new flow completely changed the dynamic. Now, when a user asks for a token, the Auth Service first verifies the password. But before doing anything else, the Auth Service calls the Compliance Service synchronously.
Note that we don’t know the exact rules inside the Compliance Service. The rules are complex, dynamic, and sometimes opaque (specific to regional regulations like China’s anti-addiction laws). We just ask: “Is this user allowed to play?”
The Compliance Service checks in the upstream system to see if the user is allowed to play. If the answer is “Yes”, the Compliance Service returns an OK. Only then does the Auth Service sign and issue the JWT.
However, if the Compliance Service says “No, the user cannot play” the Auth Service returns a 403 Forbidden error immediately. No token is generated. No session is created.
This seems like a subtle difference, but it completely killed the recreation loop.
Now, let’s go back to our adversarial user. They are playing, and their current token expires. Their scripted client attempts to recover, ignoring any “Game Over” messages. It hits the login endpoint to start a fresh session.
The Auth Service pauses. It asks the Compliance Service, “Is User 123 allowed to play?” The Compliance Service sees they have exhausted their time. It says “No.” The Auth Service rejects the login request.
The client doesn’t get a new token. Without a valid token, the Game Servers reject the next gameplay request. The user is hard-blocked. It doesn’t matter how many times they retry the request; as long as they are ineligible, they cannot get the cryptographic key required to enter the system.
By making the issuance of the token conditional on compliance, we effectively made the token issuance itself the policy boundary.
| Feature | Reactive Model (Old) | Preventative Model (New) |
|---|---|---|
| Primary Goal | Maximize Throughput / Happy Path. | Regulatory Compliance / Adversarial Path. |
| Architecture | Stateless JWT with Async Enforcement. | Conditional JWT Issuance. |
| Enforcement Point | Post-issuance (Reactive signal). | Issuance Boundary (Preventative Check). |
| Trust Model | Non-adversarial (Missed the token substitution path). | Zero-Trust Client (Server-Side Authority). |
| Communication | Asynchronous / Event-Driven. | Synchronous Eligibility Oracle. |
| Failure Mode | Risk of regulatory overage. | Fail Open (Prioritize Platform Availability). |
| Outcome | Exploitable Recreation Loop. | Robust, Audit-Proof Compliance. |
Trade-Offs: Balancing Login Latency vs. Consistent JWT Compliance
Now, if you are an architect, you are probably wincing slightly at the word “Synchronous.”
One of the main rules of microservices is to avoid tight coupling. We generally want services to be asynchronous. By forcing the Auth Service to wait for the Compliance Service to answer before it could issue a token, we were introducing a dependency.
If the Compliance Service goes down, nobody can log in. If the Compliance Service is slow, login becomes slow. We were trading Availability and Latency for Consistency.
We had to have a hard conversation about this trade-off. We measured the latency impact. The compliance check involved a quick lookup in a highly optimized state store. It added roughly 50 to 100 milliseconds to the login request.
In the world of high-frequency trading or real-time ad bidding (another area where Appliscale specializes), 100 milliseconds is an eternity. It is unacceptable.
But in the context of a user logging into a game? It is imperceptible. A player clicking “Start Game” will not notice if the loading spinner spins for 1.1 seconds instead of 1.0 seconds.
However, the risk of not doing this – the risk of regulatory fines, legal action, and bad press – was high.
But we were also mindful of the dangers of a Single Point of Failure. If the Compliance Service goes down, we don’t want to take the entire gaming platform with it.
We implemented a “Fail Open” policy for availability. If the Compliance Service is unreachable or times out, we default to allowing the user to play.
We prioritized Availability over Strict Consistency in the event of a system failure. It is better to risk a minor compliance overage during an outage than to block millions of legitimate players from accessing the game because a sub-service is down. This decision was critical to maintaining the resilience of the platform.
Beyond Gaming: Securing JWTs in High-Volume Ad Tech Environments
While this story is about gaming, the lessons here are fundamental to the work we do across the cloud ecosystem, particularly in Ad Tech.
At Appliscale, we often build scalable systems for advertising exchanges and infrastructure. Surprisingly, the problems are almost identical. In Ad Tech, we aren’t dealing with “minors” trying to play games; we are dealing with bot farms trying to generate fake impressions.
These fraudsters operate just like the adversarial gamers. They try to acquire valid session tokens so they can flood the system with fake events that look legitimate.
If you rely on a reactive model in Ad Tech – detecting the fraud after the session has started and trying to revoke the token – you have often already lost money. The bad actor has already engaged in the auction or served the impression.
The pattern we validated in this gaming case study applies directly to fraud prevention. You must validate the integrity and eligibility of the actor before you hand them the credentials to operate in your system.
Conclusion: Mastering JWT Session Control for Adversarial Environments
The biggest takeaway from this experience wasn’t about the mechanics of JWTs or the speed of databases. It was a philosophical lesson about how we design software.
As engineers, we love to design for the “Happy Path.” We imagine a user who logs in, plays by the rules, and logs out when they are told. We design for efficiency and speed under the assumption of cooperation.
But when you are operating at the scale of millions of users, you cannot assume cooperation. You are operating in an adversarial environment. You have to assume that the network calls will be intercepted—which is why we prioritize robust multi-factor authentication and secure token lifecycles – or ignored.
This reinforced a core architectural principle: You cannot outsource your regulatory compliance to the client.
You cannot rely on a stateless token to enforce stateful laws. By moving our enforcement upstream – by putting the guardrails at the very moment of issuance – we built a system that was robust not just against bugs, but against active attempts to subvert it.
It was a complex journey to get there, involving some difficult trade-offs between latency and control. But in the end, we slept much better knowing that when the system said “Time’s Up,” it actually meant it.



