reflex-dev/reflex · critical · LockExpiredError
Lock expired for token {token} while processing. Consider in
Error message
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}. What it means
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.
Source
Thrown at reflex/istate/manager/redis.py:417
token = self._coerce_token(token)
# Check that we're holding the lock.
if (
lock_id is not None
and (existing_lock_id := await self.redis.get(self._lock_key(token)))
!= lock_id
):
msg = (
f"Lock expired for token {token} while processing. Consider increasing "
f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
"or use `@rx.event(background=True)` decorator for long-running tasks. "
f"Current lock id: {existing_lock_id!r}, expected lock id: {lock_id!r}."
+ (
f" Happened in event: {event.name}"
if (event := context.get("event")) is not None
else ""
)
)
raise LockExpiredError(msg)
if not isinstance(token, BaseStateToken):
# Non-BaseState token: simple single-key write.
pickle_state = token.serialize(state)
if pickle_state:
await self.redis.set(str(token), pickle_state, ex=self.token_expiration)
return
base_state = cast(BaseState, state)
lock_key = token.lock_key
if lock_id is not None and lock_key not in self._local_leases:
time_taken = (
self.lock_expiration - (await self.redis.pttl(self._lock_key(token)))
) / 1000
if time_taken > self.lock_warning_threshold / 1000:
event_suffix = (View on GitHub (pinned to 45b8ed5ab7)
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
Example fix
# before
@rx.event
def slow_handler(self):
time.sleep(30) # lock expires during sleep
self.done = True
# after
@rx.event(background=True)
async def slow_handler(self):
await asyncio.sleep(30)
async with self:
self.done = True Defensive patterns
Strategy: retry
Try / catch
from reflex.istate.manager.redis import LockExpiredError # if exported
try:
await app.state_manager.set_state(token, state)
except LockExpiredError:
# state changes were discarded; re-fetch state and retry the event once
state = await app.state_manager.get_state(token)
# ... reapply changes, then retry set_state Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ChildrenTypeError(component=cls.__name__, child=child)
- {self._prefixes[0]}REDIS_URL is required when using the redi
- The lock warning threshold({self.lock_warning_threshold}) mu
- Backend servers are not initialized.
- Backend is not listening.
AI-assisted analysis of reflex-dev/reflex@45b8ed5ab7 (2026-08-28).
Data as JSON: /api/errors/d4777143f58383db.
Report an issue: GitHub.