Describe the bug
Under real production traffic, a small but persistent fraction of sessions end up with a row in the sessions table but no corresponding row in user_states for that (app_name, user_id). create_session() is supposed to create both atomically in one transaction, so this should not be possible — but it is happening continuously in production.
The symptom surfaces later, whenever anything calls append_event() on that session to apply a state update: DatabaseSessionService correctly raises, per _select_required_state():
ValueError: User state missing for app_name='mmvn_b2c_agent', user_id='<id>'. Session state tables should be initialized by create_session.
...but by that point the session has already been silently created without its user_state row, so every future state update for that user fails permanently until the row is manually backfilled.
Environment
google-adk version: 2.3.0 (checked latest is 2.7.0; CHANGELOG 2.4.0–2.7.0 reviewed, no entry appears related)
- Database: PostgreSQL 16 (via
asyncpg)
- Deployment: FastAPI via
get_fast_api_app(..., auto_create_session=False), uvicorn, Docker
- Session creation goes exclusively through ADK's REST endpoints — no app code touches
StorageUserState directly.
Evidence from production
SELECT count(DISTINCT s.user_id) FROM sessions s LEFT JOIN user_states us ON us.app_name=s.app_name AND us.user_id=s.user_id WHERE us.user_id IS NULL;
-- 2665 (of 19529 users = 13.6%)
Ongoing since 2025-10-30 through 2026-08-19, ~5-10/day. Reproduces on staging too (7/30 users).
What I've ruled out
- Collation/index corruption (forced seq scan still 0 rows)
- DB migration gap (legacy DB also missing these rows)
- App code deleting rows (full repo grep, nothing)
- Stale library bug (both envs on 2.3.0, SAVEPOINT logic looks correct on read)
Reproduction attempts (inconclusive)
- 8x concurrent POST same session_id: 1 wins (200, consistent), 7 get raw HTTP 500 (see related bug), no orphan ever.
- 8x concurrent POST same user, distinct session_ids: all succeed, 1 user_state row, no orphan.
- Raw-socket abrupt disconnect (40x): 0 orphans — local request completes faster than ASGI can detect disconnect and cancel.
Leading hypothesis: task-cancellation-during-commit race under real network latency, not reproducible on localhost. Related prior issue: #3328.
Related bug found while reproducing
Same session_id race → loser hits raw IntegrityError → HTTP 500 with leaked SQL, instead of clean AlreadyExistsError/409.
Expected behavior
create_session() should never leave an orphaned session without its state row, under any concurrency/disconnect scenario.
- Same-session_id race should give 409, not 500.
Describe the bug
Under real production traffic, a small but persistent fraction of sessions end up with a row in the
sessionstable but no corresponding row inuser_statesfor that(app_name, user_id).create_session()is supposed to create both atomically in one transaction, so this should not be possible — but it is happening continuously in production.The symptom surfaces later, whenever anything calls
append_event()on that session to apply a state update:DatabaseSessionServicecorrectly raises, per_select_required_state():...but by that point the session has already been silently created without its user_state row, so every future state update for that user fails permanently until the row is manually backfilled.
Environment
google-adkversion: 2.3.0 (checked latest is 2.7.0; CHANGELOG 2.4.0–2.7.0 reviewed, no entry appears related)asyncpg)get_fast_api_app(..., auto_create_session=False), uvicorn, DockerStorageUserStatedirectly.Evidence from production
Ongoing since 2025-10-30 through 2026-08-19, ~5-10/day. Reproduces on staging too (7/30 users).
What I've ruled out
Reproduction attempts (inconclusive)
Leading hypothesis: task-cancellation-during-commit race under real network latency, not reproducible on localhost. Related prior issue: #3328.
Related bug found while reproducing
Same session_id race → loser hits raw
IntegrityError→ HTTP 500 with leaked SQL, instead of cleanAlreadyExistsError/409.Expected behavior
create_session()should never leave an orphaned session without its state row, under any concurrency/disconnect scenario.