跪拜 Guibai
← All articles
Frontend · CSS

How Two Async Bugs in Redis Session Storage Wiped 40-Minute AI Conversations

By 用户05954017446 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

LLM applications that store conversation history in Redis with a fixed TTL will silently drop long-running sessions unless the expiration is refreshed on every read. A parameterized Playwright test that replays real user timing catches these bugs when unit tests cannot.

Summary

A production LLM chat app suffered intermittent session loss: users would refresh after a long conversation and find the entire history gone. Unit tests passed because they never replicated the real sequence of send, wait, render, and refresh. A Playwright end-to-end test parameterized for 5 and 15 conversation rounds exposed two bugs: `asyncio.create_task` returned HTTP responses before messages landed in Redis, and `EXPIRE` was set only once at key creation, so active sessions quietly expired mid-conversation.

The fix replaced fire-and-forget tasks with `await` and added a TTL refresh on every history read. Before the fix, 300 Playwright runs failed 84% of the time; after, all 300 passed. Two additional Playwright-specific pitfalls surfaced: missing `path=/` on cookies caused Chromium to drop the session on reload, and `<br>` tags in the DOM broke Playwright's `text=` selector across split text nodes.

Takeaways
`asyncio.create_task` in a request handler returns the HTTP response before the task completes, so a page refresh can hit Redis before the write finishes.
Setting `EXPIRE` only at key creation means a Redis list will vanish mid-conversation if the user chats longer than the TTL.
Refreshing the TTL inside the history-read endpoint keeps the key alive for active users without changing the eviction policy for abandoned sessions.
Playwright's `browser_context` isolates sessions per test and reproduces real browser quirks, such as Chromium dropping cookies that lack an explicit `path=/`.
Playwright's `text=` selector fails when the target string is split across DOM nodes by `<br>` or other inline tags; falling back to `inner_text()` and a manual assertion is more stable.
Parameterizing a Playwright test with short (5) and long (15) conversation rounds reliably surfaces timing bugs that only appear after sustained use.
Conclusions

The gap between unit tests and production is widest for stateful, async workflows: a store-then-read unit test will never catch a race between an HTTP response and a Redis write.

Cookie behavior differences across browser engines are not theoretical; Playwright reproduced a Chromium-specific cookie drop that manual curl testing missed entirely.

Many 'intermittent' session-loss bugs in LLM apps are deterministic once you control timing — the randomness comes from network and CPU jitter, not from the logic itself.

TTL-based session eviction is a common pattern, but the assumption that '7-day expiry' means '7 days from last use' is wrong unless the code explicitly renews on every access.

Concepts & terms
asyncio.create_task
Schedules a coroutine to run concurrently without waiting for it to finish. In a web request handler, this means the HTTP response can be sent before the task's side effects (like a Redis write) complete.
Redis TTL renewal
Calling `EXPIRE` on a key resets its time-to-live. Without renewal on every read or write, a key created with a 7-day TTL will expire exactly 7 days after creation, even if the user is actively chatting.
Playwright browser context
An isolated browser session with its own cookies, localStorage, and cache. Creating a fresh context per test simulates a user opening a new browser instance, avoiding state leakage between tests.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗