Description
** Please make sure you read the contribution guide and file the issues in the rigth place. **
Contribution guide.
Describe the bug
The InMemorySessionService has the following structure for storing a session
// Structure: appName -> userId -> sessionId -> Session
private final ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<String, Session>>>
sessions;
When we do deleteSession(appName, userId, sessionId) it only removes the session but leaves back an empty map for the userId and appName
What's deleted: sessionId → Session mapping
What's NOT deleted: Empty userId → {} map
What's NOT deleted: Empty appName → {} map ( This should still be fine )
Example:
BEFORE deletion:
sessions: {
"myApp": {
"user1": {
"ctx1": Session@1234
},
"user2": {
"ctx1": Session@1234
},
"user3": {
"ctx1": Session@1234
}
}
}
AFTER deletion:
sessions: {
"myApp": {
"user1": {} ← Empty map remains!
"user2": {} ← Empty map remains!
"user3": {} ← Empty map remains!
"user4": {} ← Empty map remains!
.
.
.
}
}
It can cause a potential MemoryLeak when there are a lot of users using the Agent
To Reproduce
Steps to reproduce the behavior:
- Install '...'
- Run '....'
- Open '....'
- See error
Expected behavior
An option to also delete the appName.userID map inside the InMemorySession
Screenshots
Before Deletion
After Deletion
Desktop (please complete the following information):
- OS: [e.g. iOS]
- Java version: 21
- ADK version(see maven dependency): 0.4.0
Additional context
Add any other context about the problem here.
Activity
prasadskarmarkar commentedon Dec 20, 2025
The Issue Is Valid, but the Impact Is Limited
Yes — the code does behave exactly as described.
What's Happening
In
InMemorySessionService.deleteSession()(around line 198), the session entry is removed, but the per-user map is left behind when it becomes empty:As a result, empty
userId → {}maps can accumulate in memory over time.Impact Assessment
This is better characterized as bounded memory waste, not a true memory leak:
Approximate impact:
While not ideal, the behavior is predictable and relatively minor.
InMemorySessionService Is Not Production-Ready
Per the class Javadoc (lines 45-47), this implementation is described as:
This aligns with its current behavior and limitations:
❌ No persistence (sessions lost on restart)
⚠️ Concurrent maps provide basic thread safety only
❌ Single-node only (no horizontal scaling)
❌ No distributed locking
This class is intended as a testing/development utility, not a production-grade session store.
Production Direction: DatabaseSessionService
For production deployments, the intended solution is
DatabaseSessionService, which provides:✅ Persistent storage
✅ Multi-node / horizontal scalability
✅ Proper transactional behavior with pessimistic locking
✅ No in-memory accumulation concerns
✅ Support for PostgreSQL, MySQL, H2, SQLite, and Spanner
The database-backed implementation is currently under review here:
#700
Although that PR is still being finalized, it represents the correct long-term solution for production setups, and the in-memory implementation should not be treated as a scalable or durable alternative.
This issue does not exist in the database-backed approach because sessions are stored as rows with composite keys — there are no "empty parent containers" to accumulate.
Recommendation
InMemorySessionServiceonly for local development and testingDatabaseSessionServicefor any production deploymentWould this approach address your use case?
c0k0619 commentedon Dec 23, 2025
Hello @prasadskarmarkar
Thanks for your reply.
So the thing is we are planning to use the InMemorySessionService for Production as well, as we want to use our own Custom MemoryStore
The plan is to use an interceptor design pattern to hydrate the session of the InMemorySession instead of using DBSession and then delete that session once the execution is completed.
So the number of users in our case would be more.
If you could add a new method or modify the existing method which gives us an option to also delete the appName.userID map inside the InMemorySession would be really helpful
c0k0619 commentedon Jan 7, 2026
Hello @prasadskarmarkar
Do we have an update on the above?
Additionally, I'd like to clarify the expected approach for handling multi-turn conversations in Agent-to-Agent (A2A) scenarios:
Current Understanding:
From the Google ADK and A2A implementations, it appears that messages can only be sent with a single role type. This creates a challenge when propagating conversation context between agents.
Specific Scenario:
Consider the following flow:
Question:
Should each agent independently load conversation context from a shared memory/context store rather than receiving it directly from the calling agent in the A2A Request ?
Any guidance on the intended design pattern here would be greatly appreciated.
prasadskarmarkar commentedon Jan 7, 2026
Shared Memory / Context Stores Are Not Recommended for A2A
1. Violates A2A's Core Design Principle: Agent Independence
The A2A protocol is explicitly designed for loosely coupled, distributed agent systems. Its core architectural assumptions include:
Introducing a shared memory or context store directly contradicts these principles.
Why Shared Memory Breaks A2A
Using shared memory tightly couples agents by requiring them to:
These requirements reintroduce strong coupling and shared state, which A2A intentionally avoids.
Conclusion
Also, all mesages would be sent with the role "user" that is by design. Is this causing any problems ? A2A remote agent should be able to intrepret the message properly, if you have provided proper prompts.
A2A documentation for reference :
https://a2a-protocol.org/latest/specification/#2-terminology
Message: A communication turn between a client and a remote agent, having a role ("user" or "agent") and containing one or more Parts.
c0k0619 commentedon Jan 7, 2026
Hello @prasadskarmarkar
Thank you for your response
"Also, all mesages would be sent with the role "user" that is by design. Is this causing any problems ? A2A remote agent should be able to intrepret the message properly, if you have provided proper prompts."
If every message is sent to the remote agent with the role as User in a multi turn conversation, then while creating the prompt how do we differentiate between User Request and Previous Agent Response?
For example:
If remote Agent 1 sends an A2A request to remote Agent 2 with the following requestBody as pasted below.
Now the User has already asked
What is 1 + 1?
What is 2 + 2?
And the agent has responded with
The answer is 2.
The answer is 4.
Now when the user is asking a third question: What is 3 + 3?
and this request comes to a remote agent 2, when creating the prompt shouldn't it be able to differentiate between what were the previous questions asked by the User and what did the agent respond with?
So that the LLM is not confused or doesn't hallucinate based on what has already been said and happened between the user and agents?
Sample Request Body
{ "jsonrpc": "2.0", "id": "ID123456789", "method": "message/send", "params": { "message": { "role": "user", "parts": [ { "text": "What is 1 + 1?", "kind": "text" }, { "text": "The answer is 2.", "kind": "text" }, { "text": "What is 2 + 2", "kind": "text" }, { "text": "The answer is 4.", "kind": "text" }, { "data": { "args": { "agent_name": "entry-agent" }, "name": "transfer_to_agent" }, "metadata": { "type": "function_call" }, "kind": "data" }, { "data": { "response": {}, "name": "transfer_to_agent" }, "metadata": { "type": "function_response" }, "kind": "data" }, { "text": "What is 3 + 3?", "kind": "text" } ], "messageId": "1", "contextId": "c", "kind": "message" }, "metadata": {} } }prasadskarmarkar commentedon Jan 8, 2026
Your example is hypothetical.
A2A calls are usually not intended to replay an entire multi-turn transcript verbatim to the remote agent as if it’s continuing the same chat session.
In practice, the calling agent should treat the remote agent more like a specialist service: you send it the context it needs, in a shape it can reliably interpret, rather than forwarding raw conversational history.
documentation for reference :
https://a2a-protocol.org/latest/specification/#415-role
We can modify the code to append metadata and explicitly add roles, but that is not currently supported in the A2A implementation. Unless there is a compelling and concrete requirement, it would not be appropriate to change the entire A2A message format just to accommodate this scenario.
c0k0619 commentedon Jan 8, 2026
Understood @prasadskarmarkar and it does make sense, thank you so much for breaking it down for me. I really appreciate your patience.
I had one last question to ask,
So how do we approach this the right way?
Do you mean to say when Remote Agent 1 calls Remote Agent 2 in a multi-turn conversation, is it something like the Remote Agent 1 has to summarize what has already happened between the user and the agent? and then send the summary along with the current user request to Remote Agent 2?
Even in that case, won't the remote agent 2 need to distinguish between what's the summary and what's the current user request from the incoming A2A message Payload?
prasadskarmarkar commentedon Jan 8, 2026
You can send summary or the raw multi-turn conversation to the A2A remote agent:
"Conversation history:\n\nUser: What is 1 + 1?\nModel: The answer is 2.\n\nUser: What is 2 + 2?\nModel: The answer is 4.\n\nUser: What is 3 + 3?"
However, as I mentioned earlier, the purpose of a remote agent is typically not to continue a multi-turn conversation.
If you have a specific use case in mind, please share more details about what you're trying to accomplish, and I can provide more targeted guidance on the best approach.
c0k0619 commentedon Jan 8, 2026
@prasadskarmarkar so the A2A Request from Agent 1 to Agent 2 will look something like this then?
(As attached at the very end)
Everything will still be sent with the role as User and we handle it in the Prompt?
Context:
So we are planing to create an AI Shopping Assistant which can do things like answer user questions about a particular item, or add things to cart or get user reviews, etc and it would be a Chat where in user can ask followup questions and hence the history is important.
Now the assistant will talk to a set of agents in parallel or sequential to fulfill the user requests and hence there can be more number of remote agents involved
{ "jsonrpc": "2.0", "id": "ID123456789", "method": "message/send", "params": { "message": { "role": "user", "parts": [ { "text": "Conversation history:\n\nUser: What is 1 + 1?\nModel: The answer is 2.\n\nUser: What is 2 + 2?\nModel: The answer is 4.\n\nUser: What is 3 + 3?", "kind": "text" }, { "data": { "response": {}, "name": "transfer_to_agent" }, "metadata": { "type": "function_response" }, "kind": "data" }, { "text": "What is 4 + 4?", "kind": "text" } ], "messageId": "1", "contextId": "c", "kind": "message" }, "metadata": {} } }hemasekhar-p commentedon Jan 23, 2026
Hi @c0k0619, Thank you for bringing this to our attention with such detail. After performing a deep analysis i am able to repro and I confirmed that while deleteSession correctly removes the Session object (the leaf node), it fails to prune the parent keys (userId and appName) from the nested ConcurrentHashMap structure. In a scenario with high unique user, this results in a "bounded memory leak" where thousands of empty ConcurrentHashMap objects remain in the JVM heap, as they are still reachable from the root sessions map.
This seems to be a genuine bug and we will investigate it further. Mean time as a temporary solution, you can implement the bellow utility method and call it periodically to clear out the accumulated empty maps.
c0k0619 commentedon Jan 27, 2026
Thank you @hemasekhar-p
Please keep me posted on this
c0k0619 commentedon Jan 28, 2026
@hemasekhar-p can you give us a rough timeline of by when this would be fixed?
hemasekhar-p commentedon Feb 12, 2026
Hi @c0k0619, this issue is under review. will keep you posted with updates here.
Additional Context :
The memory leak in
InMemorySessionServiceis located within thedeleteSession()method, which performs a shallow removal of the sessionId but fails to prune the parentuserIdandappNamekeys from the triple-nestedConcurrentHashMap. This results in empty maps remaining reachable in the JVM heap, preventing garbage collection and causing linear memory growth.To resolve this, the method must be updated to implement atomic hierarchical pruning using
computeIfPresentto remove parent entries only when their child maps are verified as empty.hemasekhar-p commentedon Feb 12, 2026
@sherryfox, Please have a look into it.
fix: memory leak in InMemorySessionService.deleteSession() - clean up…
fix: memory leak in InMemorySessionService.deleteSession() - clean up…
YuqiGuo105 commentedon Feb 27, 2026
Fix submitted: PR #923
#923
The Problem (as reported)
When
deleteSession()removes a session, the empty parentConcurrentHashMapobjects are left behind:With 1M users creating/deleting sessions: ~1GB leaked memory
OutOfMemoryErrorThe Fix
Replace
get()+remove()with atomiccomputeIfPresent()cleanup:Thread-safe (no extra locking needed)
Empty maps automatically removed when lambda returns
nullNo manual
pruneSessionService()utility needed anymoreTests Added
deleteSession_cleansUpEmptyParentMapssessionsmap completely emptydeleteSession_doesNotRemoveUserMapWhenOtherSessionsExistAll tests passing
cc @hemasekhar-p @c0k0619 would appreciate a review!
c0k0619 commentedon Feb 27, 2026
@hemasekhar-p @YuqiGuo105
Should we not need something similar cleanup for userState & appState as well?
YuqiGuo105 commentedon Feb 28, 2026
Hi @c0k0619
Thank you for raising this — that’s a great question.
userState and appState are intentionally designed to persist across sessions:
sessions: Ephemeral conversation context, removed when the session is deleted
userState: User-specific preferences/history that should survive session deletion (user prefixed)
appState: App-wide configuration shared across all users (app prefixed)
The memory leak fix is therefore scoped to sessions only. Deleting a session should not implicitly remove persistent user or application state, as that could lead to unexpected data loss when a user starts a new session.
If explicit cleanup of userState or appState is needed, that would be better handled via a separate API (e.g., deleteUserState(appName, userId)), rather than as part of deleteSession().
Hope this clarifies the design intent.