You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Lock Contention in EpisodicMemoryManager #908
During load testing in #837, profiling shows heavy lock contention in EpisodicMemoryManager and low requests-per-second in locust, even with a low number of users. The problem is worse as we add more users and load.
Steps to reproduce
Use the locustfile.py and profile_memmachine.sh from #837 to generate the load and profile the FastAPI processes running inside the memmachine-app Docker container.
Expected behavior
Remove serialization locks in high contention code paths.
Expect higher Requests per Second per FastAPI worker with Locust.
Environment
OS: Ubuntu Linux 25.04
MemMachine: v0.2.2
Locust load generator
Additional context
The open_or_create_episodic_memory method currently holds a global lock while performing database I/O to fetch session info. This serializes all requests to that worker.
Suggested Solution: Use a double-checked locking pattern or a per-session lock to allow concurrent I/O for different sessions.
The bug report highlights lock contention in the EpisodicMemoryManager, specifically in the open_or_create_episodic_memory method (named slightly differently in source: open_episodic_memory). Profiling during load testing shows poor scalability, especially with increasing concurrent users. Lock contention serializes requests even when different users/sessions are active, leading to suboptimal requests-per-second with Locust.
Relevant Code
The portion of the code where locking is performed is here (see full context for all details):
Global Lock (self._lock) Used for All Sessions: The method acquires a single asyncio lock no matter what session_key is requested. If any user is opening a session (including slow database I/O or creation), all other calls—even for entirely different session_keys—must wait.
Slow Database/Initialization Under Lock: Database I/O and any slow or eventful initialization logic for a session are run while holding the lock, blocking all other requests.
High Contention Under Concurrency: With many users (and especially with more FastAPI workers or async routines), this results in all requests bottlenecked on this single lock, causing the serialization, low throughput, and poor scalability described.
Solution: Scalable Locking Design
To scale well for hundreds of users and multiple FastAPI workers, let’s redesign the locking as follows:
Key Requirements
Concurrency for Different Session Keys: Opening episode memory for session_key_1 must not block session_key_2.
Critical-Section Granularity: The lock should apply only when necessary (e.g., when creating or inserting into the cache) and only per session-key.
Avoid Orphaned Instances: If two routines try to create memory for the same session concurrently, ensure only one wins and the instance is not leaked or duplicated.
Recommended Approach
Per-Session-Key Locking
Use a mapping (e.g., a collections.defaultdict or WeakValueDictionary) from session_key → asyncio.Lock.
Acquire the lock for just the requested session_key during memory open/create operations.
Once created and cached, releasing the per-session lock allows other requests for that session to proceed without blocking unrelated sessions.
Double-Checked Locking Pattern
Before entering the per-key lock, check if the instance is already cached.
After acquiring the lock, check the cache again—another coroutine might have created the instance while this one was waiting.
Only run the database load/creation path inside the per-session lock and only if no matching instance was created in the meantime.
Cleanup: Ensure the per-session-key lock mapping does not grow unboundedly; e.g., remove locks after session memory instances have expired or are cleaned up.
Example Skeleton
classEpisodicMemoryManager:
def__init__(...):
...
self._locks=defaultdict(asyncio.Lock) # or some other mappingasyncdefopen_episodic_memory(self, session_key: str):
# First check outside lockinstance=self._instance_cache.get(session_key)
ifinstanceisnotNone:
returninstance# Acquire a lock specific to the session_keylock=self._locks[session_key]
asyncwithlock:
# Check again after acquiring lockinstance=self._instance_cache.get(session_key)
ifinstanceisnotNone:
returninstance# Instance does not exist, do DB call and createsession_info=awaitself._session_data_manager.get_session_info(session_key)
...
# Cache the instanceself._instance_cache.set(session_key, new_instance)
returnnew_instance# Optionally: clean up lock for session_key if not needed anymore
For Multi-Worker/Process Scenarios
If using multiple FastAPI workers (i.e., separate processes), Python in-memory locks and cache are not shared between workers. This code fixes the async/task-level concurrency, but global session-level concurrency must be coordinated at the DB/archetype layer or using a distributed lock/service if necessary for truly “single-session” semantics. For most cases, optimistic concurrency and DB row-level locking are sufficient if you ensure get_session_info and creation are idempotent.
Workaround(s) if Immediate Refactor Not Possible
Reduce Lock Scope: Move only the cache read/write and creation logic inside the lock; do slow I/O operations (session checks, parameter loading) outside the global lock if possible.
Increase FastAPI Worker Count: If DB and downstream services can take the load, more independent processes can help—but only up to the level the DB can handle, and only if their cache policies don’t cause ongoing cross-worker cache misses and repeated contention.
Summary
To resolve the bug:
Replace the single global lock with a per-session lock.
Only hold the lock when creating or inserting a session-specific memory if it does not already exist.
Use double-checked locking to avoid redundant creation.
For full scalability across FastAPI workers/processes, ensure the data store for session info is robust and initialize actions are repeatable and safe.
Activity
sscargal commentedon Jan 7, 2026
Understanding the Issue
The bug report highlights lock contention in the
EpisodicMemoryManager, specifically in theopen_or_create_episodic_memorymethod (named slightly differently in source:open_episodic_memory). Profiling during load testing shows poor scalability, especially with increasing concurrent users. Lock contention serializes requests even when different users/sessions are active, leading to suboptimal requests-per-second with Locust.Relevant Code
The portion of the code where locking is performed is here (see full context for all details):
View the code on GitHub
Cause of the Problem
self._lock) Used for All Sessions: The method acquires a single asyncio lock no matter what session_key is requested. If any user is opening a session (including slow database I/O or creation), all other calls—even for entirely different session_keys—must wait.Solution: Scalable Locking Design
To scale well for hundreds of users and multiple FastAPI workers, let’s redesign the locking as follows:
Key Requirements
session_key_1must not blocksession_key_2.Recommended Approach
Per-Session-Key Locking
collections.defaultdictorWeakValueDictionary) fromsession_key→asyncio.Lock.Double-Checked Locking Pattern
Cleanup: Ensure the per-session-key lock mapping does not grow unboundedly; e.g., remove locks after session memory instances have expired or are cleaned up.
Example Skeleton
For Multi-Worker/Process Scenarios
If using multiple FastAPI workers (i.e., separate processes), Python in-memory locks and cache are not shared between workers. This code fixes the async/task-level concurrency, but global session-level concurrency must be coordinated at the DB/archetype layer or using a distributed lock/service if necessary for truly “single-session” semantics. For most cases, optimistic concurrency and DB row-level locking are sufficient if you ensure
get_session_infoand creation are idempotent.Workaround(s) if Immediate Refactor Not Possible
Summary
To resolve the bug:
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager
[GH-908] Lock contention in EpisodicMemoryManager (#944)
[GH-908] Lock contention in EpisodicMemoryManager (#944)
[MemMachineGH-908] Lock contention in EpisodicMemoryManager (MemMachi…