{"record":{"id":"d4777143f58383db","repo":"reflex-dev/reflex","slug":"lock-expired-for-token-token-while-processing-c","errorCode":null,"errorMessage":"Lock expired for token {token} while processing. Consider increasing `app.state_manager.lock_expiration` (currently {self.lock_expiration}) or use `@rx.event(background=True)` decorator for long-running tasks. Current lock id: {existing_lock_id!r}, expected lock id: {lock_id!r}.","messagePattern":"Lock expired for token (.+?) while processing\\. Consider increasing `app\\.state_manager\\.lock_expiration` \\(currently (.+?)\\) or use `@rx\\.event\\(background=True\\)` decorator for long-running tasks\\. Current lock id: (.+?), expected lock id: (.+?)\\.","errorType":"exception","errorClass":"LockExpiredError","httpStatus":null,"severity":"critical","filePath":"reflex/istate/manager/redis.py","lineNumber":417,"sourceCode":"        token = self._coerce_token(token)\n        # Check that we're holding the lock.\n        if (\n            lock_id is not None\n            and (existing_lock_id := await self.redis.get(self._lock_key(token)))\n            != lock_id\n        ):\n            msg = (\n                f\"Lock expired for token {token} while processing. Consider increasing \"\n                f\"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) \"\n                \"or use `@rx.event(background=True)` decorator for long-running tasks. \"\n                f\"Current lock id: {existing_lock_id!r}, expected lock id: {lock_id!r}.\"\n                + (\n                    f\" Happened in event: {event.name}\"\n                    if (event := context.get(\"event\")) is not None\n                    else \"\"\n                )\n            )\n            raise LockExpiredError(msg)\n\n        if not isinstance(token, BaseStateToken):\n            # Non-BaseState token: simple single-key write.\n            pickle_state = token.serialize(state)\n            if pickle_state:\n                await self.redis.set(str(token), pickle_state, ex=self.token_expiration)\n            return\n\n        base_state = cast(BaseState, state)\n\n        lock_key = token.lock_key\n\n        if lock_id is not None and lock_key not in self._local_leases:\n            time_taken = (\n                self.lock_expiration - (await self.redis.pttl(self._lock_key(token)))\n            ) / 1000\n            if time_taken > self.lock_warning_threshold / 1000:\n                event_suffix = (","sourceCodeStart":399,"sourceCodeEnd":435,"githubUrl":"https://github.com/reflex-dev/reflex/blob/45b8ed5ab735f8a56bbb09a42384f030eb0208e7/reflex/istate/manager/redis.py#L399-L435","documentation":"While flushing state back to Redis inside set_state, the manager discovered the distributed lock for the token was no longer owned by this process (the lock id changed), meaning the lock expired mid-event and another writer may have taken it. Writing would clobber concurrent updates, so LockExpiredError is raised instead. The message points at raising lock_expiration or moving long work into a background event.","triggerScenarios":"An event handler holds a state token longer than state_manager.lock_expiration (e.g. sleeps, slow external API calls, big loops inside a regular event), so the Redis lock TTL lapses before set_state runs; heavy multi-user load amplifies handler duration past the configured expiration.","commonSituations":"Default lock_expiration too small for handlers that call slow third-party APIs; CPU-heavy loops in normal (non-background) event handlers; production traffic spikes making handlers exceed the TTL; tests that assert on lock behavior with artificially tiny expirations.","solutions":["Move long-running logic into @rx.event(background=True) and keep the wrapped section short (async with self)","Increase app.state_manager.lock_expiration (e.g. StateManagerRedis(lock_expiration=60)) to comfortably exceed worst-case handler duration","Profile/trim the slow work inside the handler so it finishes well under the TTL","Optionally catch LockExpiredError at the boundary and retry the event once, accepting that state changes were discarded"],"exampleFix":"# before\n@rx.event\ndef slow_handler(self):\n    time.sleep(30)  # lock expires during sleep\n    self.done = True\n\n# after\n@rx.event(background=True)\nasync def slow_handler(self):\n    await asyncio.sleep(30)\n    async with self:\n        self.done = True","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from reflex.istate.manager.redis import LockExpiredError  # if exported\ntry:\n    await app.state_manager.set_state(token, state)\nexcept LockExpiredError:\n    # state changes were discarded; re-fetch state and retry the event once\n    state = await app.state_manager.get_state(token)\n    # ... reapply changes, then retry set_state","preventionTips":["Use @rx.event(background=True) for anything that may run long","Set lock_expiration comfortably above worst-case handler latency","Monitor handler duration and lock warning logs; raise expiration proactively"],"tags":["reflex","redis","distributed-lock","timeout","concurrency"],"backgroundTag":"distributed-lock-timeout","analyzedSha":"45b8ed5ab735f8a56bbb09a42384f030eb0208e7","analyzedAt":"2026-08-28T19:25:27.644Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}