Skip to content

perf(session): write on lookup only when sliding expiration renews - #279

Merged
allen0099 merged 1 commit into
masterfrom
perf/session-lookup-no-write-115
Sep 27, 2026
Merged

allen0099 merged 1 commit into
masterfrom
perf/session-lookup-no-write-115

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #115.

What

SessionManager.get_session() saved the session on every lookup just to record last_accessed. Every authenticated request therefore cost a backend write, and a request that also modified the session cost two (the lookup, then the middleware's save).

  • get_session() now saves only when sliding expiration renewed the session. That is the only kind of lookup that changes expires_at, and so the backend TTL.
  • The new keyword-only touch=True saves on every lookup, for callers who need the stored last_accessed to be exact.
  • Session entries store the constant fingerprint "session" instead of a SHA-256 of the payload. Nothing reads session fingerprints: the monitoring routes skip non-HTTP keys, and sessions don't use delete_if_equals.

These are all unchanged:

  • expiry checks (an expired session is still saved as EXPIRED and raises);
  • absolute_timeout;
  • IP / User-Agent binding;
  • invalidation;
  • regenerate_session_id;
  • the middleware's clear()/logout logic, rotation detection and saving of modified data.

Behaviour change

The last_accessed on the returned session is still the current time. The stored last_accessed is now the time of the last write (create, modify, renew, regenerate or touch=True), not the time of the last lookup. Nothing in the library reads it.

A side benefit: a read-only request no longer rewrites the session it loaded at the start. So it can no longer bring back a session that a concurrent request deleted, or overwrite that request's changes with stale data. The same window still exists for requests that modify the session: the middleware saves the copy it loaded. That is out of scope here.

Tests

tests/session/test_lookup_writes.py wraps MemoryBackend in a spy that records every set:

  • a plain lookup makes 0 writes, with sliding expiration on and off;
  • a renewal makes exactly 1 write and moves the stored expires_at;
  • touch=True makes 1 write and stores last_accessed;
  • through FastAPICacheXSessionMiddleware, a read-only request makes 0 writes and a modifying request makes exactly 1;
  • the deprecated SessionMiddleware lookup makes 0 writes;
  • the stored fingerprint is "session".

Mutation check: each change below was applied to manager.py and the full suite was run.

Mutation Tests that fail
Restore the unconditional save 5 new tests (both plain-lookup cases, touch, both middleware tests)
Drop the save on renewal The new renewal test, plus the existing test_no_renewal_once_expiry_reaches_absolute_timeout
Ignore touch Only the touch test
Hash the fingerprint again Only the fingerprint test

uv run pytest -q: 821 passed, 191 skipped. The live Redis/Memcached tests were skipped and are still to be run. pre-commit run --all-files is clean (the uv-lock hook was skipped because it only bumps unrelated upstream pins; uv lock --check passes). Both strict docs builds (EN and zh-TW) pass.

CHANGELOG

Changed

  • Session lookups no longer write to the backend unless sliding expiration renewed the session. SessionManager.get_session() used to save the session on every call to record last_accessed, so each authenticated request cost a write (two if it also modified the session). The stored last_accessed is now updated only when the session is written (created, modified, renewed or regenerated); pass get_session(..., touch=True) to save it on every lookup. Session entries also store a constant fingerprint instead of hashing the payload. (Session lookups write to the backend on every request #115)

get_session() saved the session on every lookup to record last_accessed,
so each authenticated request cost a backend write, and one that also
modified the session cost two. It now saves only when sliding expiration
renewed the session, which is the only lookup that changes the expiry or
backend TTL. The keyword-only touch=True keeps the old behaviour for
callers that need the stored last_accessed to be exact.

Session entries are never compared by fingerprint, so they store the
constant "session" instead of a SHA-256 of the payload.

Closes #115
@allen0099 allen0099 added this to the 0.3.8 milestone Sep 26, 2026
@allen0099 allen0099 added enhancement New feature or request session Session management subsystem labels Sep 26, 2026
@allen0099
allen0099 merged commit f832138 into master Sep 27, 2026
11 checks passed
@allen0099
allen0099 deleted the perf/session-lookup-no-write-115 branch September 27, 2026 10:01
allen0099 added a commit that referenced this pull request Sep 27, 2026
Copies the CHANGELOG sections of #207, #208, #209, #211, #212, #272,
#273, #274, #275, #276, #279 and #284 into Unreleased. The #273 entry
drops expire_if_equals from its list of methods that changed, since that
primitive is new in 0.3.8.
allen0099 added a commit that referenced this pull request Sep 27, 2026
Copies the CHANGELOG sections of #207, #208, #209, #211, #212, #272,
#273, #274, #275, #276, #279 and #284 into Unreleased. The #273 entry
drops expire_if_equals from its list of methods that changed, since that
primitive is new in 0.3.8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request session Session management subsystem

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session lookups write to the backend on every request

1 participant