Skip to content

DatabaseSessionService.append_event: unnecessary FOR UPDATE lock on app_states/user_states when no state delta exists #4655

Closed

Description

@rusherman

Description

DatabaseSessionService.append_event() unconditionally acquires SELECT ... FOR UPDATE on both app_states and user_states tables, even when the event carries no state delta for those scopes.

This was partially addressed by #764, which fixed the unnecessary UPDATE (merge) side. However, the SELECT ... FOR UPDATE (row-level lock acquisition) still happens unconditionally.

Problem

In database_session_service.py (lines 553-578 in v1.26.0), _select_required_state is called with use_row_level_locking=use_row_level_locking for both app_states and user_states, regardless of whether the event actually has app: or user: prefixed state deltas.

storage_app_state = await _select_required_state(
    sql_session=sql_session,
    state_model=schema.StorageAppState,
    predicates=(schema.StorageAppState.app_name == session.app_name,),
    use_row_level_locking=use_row_level_locking,  # Always True on PostgreSQL
    ...
)

Since app_states is keyed by app_name alone, all concurrent append_event calls within the same app serialize on this single row lock, even if none of them have app: state deltas. In practice, the vast majority of events only carry session-scoped state, making this lock contention unnecessary.

Impact

For applications with moderate concurrency (e.g., multiple agents within the same app writing events simultaneously), this creates a significant bottleneck:

  • All append_event calls for the same app_name are serialized by the FOR UPDATE lock on app_states
  • The user_states lock similarly serializes all calls for the same (app_name, user_id) pair
  • This happens even when the event has zero state delta (e.g., plain conversation events)

Suggested Fix

Only acquire FOR UPDATE when the event actually has a delta for the corresponding scope:

# Pre-analyze deltas before the transaction
has_app_delta = bool(state_deltas.get("app")) if state_deltas else False
has_user_delta = bool(state_deltas.get("user")) if state_deltas else False

# Then in the transaction:
storage_app_state = await _select_required_state(
    sql_session=sql_session,
    state_model=schema.StorageAppState,
    predicates=(schema.StorageAppState.app_name == session.app_name,),
    use_row_level_locking=use_row_level_locking and has_app_delta,
    ...
)
storage_user_state = await _select_required_state(
    sql_session=sql_session,
    state_model=schema.StorageUserState,
    predicates=(...),
    use_row_level_locking=use_row_level_locking and has_user_delta,
    ...
)

The state delta can be extracted before the DB transaction using extract_state_delta() since it only reads from the event object and has no side effects.

Environment

  • google-adk version: 1.26.0
  • Database: PostgreSQL (with asyncpg)
  • Python: 3.12

Activity

  1. added
    services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc
    on Feb 28, 2026
  2. added theissue type on Feb 28, 2026
  3. added a commit that references this issue on Feb 28, 2026
    2715a32
  4. surajksharma07 commented on Mar 1, 2026

    @surajksharma07
    Collaborator

    @rusherman database_session_service.py:557 and :571 pass use_row_level_locking=use_row_level_locking unconditionally to _select_required_state(), issuing SELECT ... FOR UPDATE on both app_states and user_states even though extract_state_delta() isn't called until line 604.

    The fix is to pre-compute has_app_delta / has_user_delta from the event before entering the transaction and change those two call sites to use_row_level_locking and has_app_delta and use_row_level_locking and has_user_delta respectively — extract_state_delta() is a pure synchronous read so it's safe to move earlier.

    PR #4661 has the right approach.

    Team is looking more into it.

  5. added a commit that references this issue on Mar 2, 2026
    3d9ebbe
  6. self-assigned this
    on Mar 2, 2026
  7. removed their assignment
    on Mar 3, 2026
  8. added a commit that references this issue on Mar 3, 2026
    d61846f
  9. added a commit that references this issue on Aug 11, 2026
    fe0818d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions