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

  1. Move long-running logic into @rx.event(background=True) and keep the wrapped section short (async with self)
  2. Increase app.state_manager.lock_expiration (e.g. StateManagerRedis(lock_expiration=60)) to comfortably exceed worst-case handler duration
  3. Profile/trim the slow work inside the handler so it finishes well under the TTL
  4. 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

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


AI-assisted analysis of reflex-dev/reflex@45b8ed5ab7 (2026-08-28). Data as JSON: /api/errors/d4777143f58383db. Report an issue: GitHub.