Session Isolation & the Security Boundary
One microVM per session, and who owns the user mapping
- Explain the per-session microVM isolation model and why it matters for non-deterministic, privileged agents
- Trace the session lifecycle states and the four termination triggers, and configure `idleRuntimeSessionTimeout` / `maxLifetime` via `LifecycleConfiguration`
- Design for ephemeral-by-default state, choosing session storage or AgentCore Memory when you need durability
- Generate and reuse a ≥33-character `runtimeSessionId` to preserve context and microVM affinity and avoid cold starts
- Own the user→session mapping in your backend, including per-user session caps, since AWS does not
- Send the correct session header for each protocol (HTTP vs MCP) and echo the returned session ID
AgentCore Runtime's headline guarantee is hard session isolation: every user session runs in its own dedicated microVM with isolated CPU, memory, and filesystem, torn down and sanitized on termination so nothing leaks between sessions. This lesson walks the session lifecycle and its timeouts, why everything is ephemeral by default, how session-ID affinity keeps you on the same warm microVM, and the one boundary AWS deliberately leaves to you: AgentCore never maps sessions to users — your backend owns that.
- 1One microVM per session: the security boundary
- 2Lifecycle states and the four ways a session dies
- 3Ephemeral by default: durability is your job
- 4Session IDs: context, affinity, and avoiding cold starts
- 5AWS does not map users to sessions — your backend does
- 6Session headers differ by protocol
One microVM per session: the security boundary
Agents are a uniquely hostile thing to multi-tenant. They are non-deterministic, they hold stateful reasoning context across turns, and they perform privileged tool operations on a user's behalf — reading files, calling APIs, running code. Sharing a process between two users' sessions would be a data-leak waiting to happen.
AgentCore Runtime's answer is the strongest isolation primitive it can offer: every user session gets its own dedicated microVM with isolated compute, memory, and filesystem. AWS's developer guide calls it a "dedicated microVM"; the runtime GA blog and surrounding ecosystem write-ups describe it specifically as a Firecracker microVM (treat the exact hypervisor name as ecosystem-sourced rather than a devguide guarantee).
The second half of the guarantee is the teardown: on session termination the microVM is destroyed and its memory sanitized, so there is no cross-session contamination. Session A cannot read session B's in-memory secrets, scratch files, or conversation — because session B never shared a kernel, a heap, or a disk with it in the first place.
This is the architectural difference from a classic stateless FaaS. AgentCore provisions the microVM per session and keeps it alive across many invocations (up to 8 hours), rather than spinning an anonymous worker per request. The isolation unit is the session, not the request.
Key insight
Why session-level, not request-level, isolation
A request-per-worker model (Lambda) is fine for stateless calls but wrong for agents, which carry reasoning state between turns. AgentCore keeps the same microVM warm for the whole session so turn 5 sees the working memory from turn 1 — while still fencing that state off from every other user. You get statefulness within a session and isolation across sessions, simultaneously.
Note
Hardware ceiling per session
Each session's microVM has a fixed per-session hardware ceiling — 2 vCPU / 8 GB as of writing, and not adjustable (a quota, so confirm the live number on the AgentCore limits page). Plan memory-heavy work (large context windows, in-process vector stores) against that ceiling rather than assuming you can scale a single session vertically.
Lifecycle states and the four ways a session dies
A session moves through three observable states:
- Active — currently processing an invoke, or running tracked background tasks.
- Idle — provisioned and holding context, but not doing work right now (waiting for the next turn).
- Stopped / Terminated — the microVM has been torn down and its memory sanitized.
Four things move a session into the terminated state:
| Trigger | Default | Notes |
|---|---|---|
| Idle timeout | 15 min | No invoke and no tracked background work for this long. |
| Max lifetime | 8 hours | Hard ceiling on total session compute, counted from first invoke. |
Explicit StopRuntimeSession | — | Data-plane call to tear down a session deterministically. |
| Unhealthy | — | The container's /ping health check fails. |
The two timeouts are configurable through LifecycleConfiguration on the agent runtime — idleRuntimeSessionTimeout for the idle window and maxLifetime for the ceiling (both [VOLATILE] defaults — verify on the live quotas page):
{
"lifecycleConfiguration": {
"idleRuntimeSessionTimeout": 900,
"maxLifetime": 28800
}
}A subtle but important rule: a new invoke with the same runtimeSessionId after a stop provisions a fresh microVM with a new ≤8-hour lifecycle. The session ID itself "remains valid until the AgentCore Runtime ARN is deleted" — so reusing an ID after termination is allowed, it just doesn't resurrect the old microVM's in-memory state.
Watch out
Blocking the entrypoint can kill a session early
The idle timeout is enforced against the /ping health thread. If a long-running call blocks inside @app.entrypoint, it starves /ping, the session reads as idle (or unhealthy), and it gets killed at ~15 minutes even though it's actually working. Offload long work to threads/async and keep the health check reporting HealthyBusy. (Covered in depth in the streaming & async lesson.)
Ephemeral by default: durability is your job
Because the microVM is destroyed on termination, anything you keep in memory or on the local disk lives only for that microVM's lifecycle. When the session stops — idle timeout, max lifetime, explicit stop, or crash — that state is gone and sanitized. This is the single most common mental-model mistake: treating a session's RAM or /tmp as if it were a database.
There are two first-class ways to make state survive a teardown:
- Session storage — a persistent mount path that survives stop/resume of the same session ID, for filesystem state your agent reads and writes (scratch files, downloaded artifacts, working directories). It is capped at 1 GB per session ([VOLATILE] — verify on the limits page).
- AgentCore Memory — the managed service for durable conversational state (short-term turns and long-term extracted facts), independent of any one microVM. This is what you reach for when a user comes back tomorrow and expects the agent to remember them.
A useful rule of thumb:
| Need | Use | Survives session teardown? |
|---|---|---|
| Working scratch within a turn / request | microVM RAM or local disk | No — gone on termination |
| Files that must outlive stop/resume of this session | Session storage (persistent mount) | Yes, per session ID |
| Conversation history & extracted user facts | AgentCore Memory | Yes, independent of microVM |
If you find yourself writing "save to /tmp and hope it's there next turn," stop — that only works within a live microVM and breaks the moment the session is recycled.
Tip
Map your state before you deploy
List every piece of state your agent touches and tag it: per-turn scratch (RAM/disk, fine to lose), per-session files (session storage), or durable user context (AgentCore Memory). Anything you can't afford to lose at the 8-hour mark or on an idle timeout must live in session storage or Memory, never in the microVM alone.
Session IDs: context, affinity, and avoiding cold starts
The session ID does double duty. It is the key that ties related invocations into one conversation, and it is what routes you back to the same warm microVM — what AWS calls microVM "stickiness" or affinity.
The rules:
- Generate one unique ID per user/conversation. It must be at least 33 characters (commonly cited; confirm against the live API reference). A UUID is 36 characters, so a plain
uuid4()satisfies the minimum. - Reuse the same ID for every invoke in that conversation. This keeps your reasoning context and is what pins you to the already-provisioned microVM.
- Echo the returned ID. The client must capture the session ID returned in the response and send it back on the next call. If you let each request fall back to a new ID, every request may land on a fresh microVM — paying cold-start latency each time and losing affinity.
From boto3, you supply runtimeSessionId explicitly on the data-plane invoke_agent_runtime call:
import json, uuid, boto3
client = boto3.client("bedrock-agentcore") # data plane
session_id = str(uuid.uuid4()) # 36 chars >= 33 minimum
resp = client.invoke_agent_runtime(
agentRuntimeArn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/MyAgent-XXXX",
runtimeSessionId=session_id, # REUSE this for the whole conversation
payload=json.dumps({"prompt": "Tell me a joke"}).encode(),
qualifier="DEFAULT",
)
print(json.loads("".join(c.decode("utf-8") for c in resp.get("response", []))))
# Next turn in the same conversation -> same session_id -> same warm microVM
resp2 = client.invoke_agent_runtime(
agentRuntimeArn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/MyAgent-XXXX",
runtimeSessionId=session_id, # same ID, not a new uuid4()
payload=json.dumps({"prompt": "Now make it about databases"}).encode(),
qualifier="DEFAULT",
)Inside the runtime, your code can read the active session ID from the request context — e.g. via context.session_id on the optional RequestContext argument — which is handy for keying session storage paths or correlating logs.
Watch out
A new uuid4() per request is an anti-pattern
Generating a fresh runtimeSessionId on every call is the classic cold-start bug: you lose context AND microVM affinity, so each request provisions a new microVM. Generate the ID once per conversation, persist it (see the next section), and send it on every turn.
AWS does not map users to sessions — your backend does
Here is the boundary that trips up every team building their first multi-user agent: AgentCore does not maintain any relationship between users and session IDs. The devguide is explicit — "your client backend should maintain the relationship between users and their session IDs."
A session ID to AWS is just an opaque routing/affinity key. It does not know that session a1b2... belongs to Alice, that Alice may have three concurrent conversations, or that your product limits each user to five active sessions. All of that is yours to own. Concretely, your backend is responsible for:
- The user→session mapping — storing which session ID(s) belong to which authenticated user, in your own datastore.
- Per-user session caps — deciding and enforcing how many concurrent sessions a user may hold (AgentCore only enforces an account-level active session workloads quota, not per-user limits).
- Authorization — verifying that the caller actually owns a session ID before forwarding their invoke with it. Nothing stops a client from sending someone else's ID; you must reject that.
A minimal backend mapping pattern:
import uuid
# Pseudocode: your service, your datastore, your auth
MAX_SESSIONS_PER_USER = 5
def get_or_create_session(user_id: str, conversation_id: str) -> str:
record = sessions_table.get(user_id, conversation_id)
if record:
return record["runtime_session_id"] # reuse -> context + affinity
active = sessions_table.count_active(user_id)
if active >= MAX_SESSIONS_PER_USER:
raise PermissionError("Per-user session cap reached")
session_id = str(uuid.uuid4()) # >= 33 chars
sessions_table.put(user_id, conversation_id, session_id)
return session_id
def authorize(user_id: str, session_id: str) -> None:
if not sessions_table.owns(user_id, session_id):
raise PermissionError("Session does not belong to this user")The microVM isolation guarantees that sessions can't leak into each other. Owning the mapping guarantees that the right user reaches the right session in the first place — and that is squarely your responsibility, not AWS's.
Watch out
Treat the session ID as a capability
Because AWS doesn't bind a session to a user, possession of a valid session ID is effectively a key to that session's context and warm microVM. Never expose raw session IDs to untrusted clients without an authorization check, and don't derive them from guessable values. Mint a random ID, store the user binding server-side, and check ownership on every invoke.
Session headers differ by protocol
How the session ID travels on the wire depends on which protocol your runtime declares. The data-plane API exposes runtimeSessionId as a parameter, but at the HTTP layer it maps to a protocol-specific header — and the header name is not the same for MCP.
| Protocol | Session header (exact name) |
|---|---|
| HTTP | X-Amzn-Bedrock-AgentCore-Runtime-Session-Id |
| MCP | Mcp-Session-Id |
| A2A | X-Amzn-Bedrock-AgentCore-Runtime-Session-Id |
| AG-UI | X-Amzn-Bedrock-AgentCore-Runtime-Session-Id |
If you call an HTTP-protocol runtime with a raw HTTPS request (which you must do when the agent uses OAuth inbound auth, since the AWS SDK can't sign those), you set the session header yourself:
curl -X POST "$AGENT_INVOKE_URL" \
-H "Authorization: Bearer $OAUTH_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id: 9f8c5b6a-0d1e-4f23-8a77-1c2d3e4f5a6b" \
-d '{"prompt": "What is the weather?"}'MCP is the special case. When you host an MCP server inside Runtime (port 8000, POST /mcp, streamable-http), the platform injects and returns Mcp-Session-Id. Your server must accept the platform-generated ID and must not reject it — a common failure in stateless MCP mode is a server that tries to mint or validate its own session ID and rejects the platform's. Default MCP servers to stateless_http=True and let the platform own the session header:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(host="0.0.0.0", stateless_http=True) # accept the platform Mcp-Session-Id
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
mcp.run(transport="streamable-http") # served at /mcp on port 8000Example
Same session, two operations
InvokeAgentRuntime (agent reasoning) and InvokeAgentRuntimeCommand (a deterministic shell command) both operate on the same session and microVM — the command sees the exact same container, filesystem, and environment as the agent. Reuse the same session ID across both to inspect or manipulate the live microVM the agent is running in.
Try it: Prove session isolation, affinity, and the user-mapping boundary
Goal: experience the microVM-per-session model first-hand — see context follow a reused session ID, see it vanish with a fresh one, and build the user→session mapping AWS won't.
Assumes you already have a deployed agent runtime (from the next lesson) and the bedrock-agentcore data-plane client configured.
- Write per-session scratch. In your agent's
@app.entrypoint, on a prompt like"remember X", write a file under the session's working directory and readcontext.session_idto key it. On"recall", read it back. Deploy. - Prove affinity. From boto3, generate
session_id = str(uuid.uuid4())(confirmlen(session_id) >= 33). Callinvoke_agent_runtime(... runtimeSessionId=session_id ...)with"remember the number 42", then a second invoke with the SAMEsession_idand"recall the number". Confirm it remembers — you stayed on the same warm microVM. - Prove ephemerality / no affinity. Repeat step 2 but generate a NEW
uuid4()for the second call. Confirm it does NOT remember, and note any extra latency (cold start on a fresh microVM). - Tune the lifecycle. Update the runtime with a
LifecycleConfigurationloweringidleRuntimeSessionTimeout(e.g. to 60s). Invoke once, wait past the idle window, then reuse the same ID and confirm a fresh microVM was provisioned (scratch from step 2 is gone). Restore the default afterward. Treat the exact field/units as volatile — check the limits page. - Build the mapping AWS won't. Stand up a tiny backend table keyed by
(user_id, conversation_id) -> runtime_session_id. Implementget_or_create_sessionwith aMAX_SESSIONS_PER_USERcap and anauthorize(user_id, session_id)ownership check. Demonstrate that user B sending user A's session ID is rejected by YOUR code (AgentCore would happily route it). - Header check. If your agent is HTTP-protocol, make a raw
curlinvoke settingX-Amzn-Bedrock-AgentCore-Runtime-Session-Idyourself and confirm it behaves identically to the SDK path. (If you host an MCP server instead, confirm it accepts the platform-providedMcp-Session-Idrather than minting its own.) - Reflect (3–5 sentences). What state did you keep in the microVM vs. what would have to move to session storage or AgentCore Memory in production? What per-user cap and authorization rules does your backend need that AWS does not provide?
Key takeaways
- 1Every user session runs in its own dedicated microVM (Firecracker per AWS write-ups) with isolated CPU/memory/filesystem; on termination the microVM is destroyed and memory sanitized, so there is no cross-session contamination. The isolation unit is the session, not the request.
- 2Sessions are Active, Idle, or Stopped/Terminated. Four triggers terminate them: idle timeout (default 15 min), max lifetime (default 8 hours), explicit `StopRuntimeSession`, or unhealthy `/ping` — with the two timeouts tunable via `idleRuntimeSessionTimeout` and `maxLifetime` in `LifecycleConfiguration`.
- 3State is ephemeral by default: in-memory and local-disk data live only for the microVM's lifecycle. For durable filesystem state use session storage (persistent mount, ~1 GB/session); for durable conversation use AgentCore Memory.
- 4The `runtimeSessionId` must be ≥33 chars (a UUID's 36 works) and should be reused for the whole conversation — it preserves context AND keeps you on the same warm microVM. A fresh ID per request loses both and pays repeated cold starts; the client must echo the returned ID.
- 5AWS does NOT map users to sessions. Your backend owns the user→session mapping, per-user session caps, and authorization — the account-level quota is the only limit AWS enforces.
- 6The session header is protocol-specific: `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` for HTTP/A2A/AG-UI, but `Mcp-Session-Id` for MCP — and an in-Runtime MCP server must accept the platform-generated `Mcp-Session-Id`, not reject it.
- 7A new invoke with a reused session ID after a stop provisions a fresh microVM (new ≤8h lifecycle); the ID stays valid until the agent runtime ARN is deleted.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.An engineer worries that two users' agent sessions could leak data into each other through shared process memory or temp files. What is AgentCore Runtime's actual isolation model?
2.A long-running agent must keep working for two hours and the team is surprised when sessions die at the 15-minute mark, and separately when state vanishes after a session stops. Which statement is correct?
3.A client generates a new `uuid4()` for `runtimeSessionId` on every single invoke. What is the consequence?
4.Your multi-user product needs to ensure each user only sees their own sessions and is capped at five concurrent ones. Where does that logic live?
Go deeper
Hand-picked sources to keep learning
The canonical source: per-session microVM isolation, the 'your backend owns user→session mapping' rule, lifecycle and session IDs.
Big-picture model: serverless per-session microVMs, up to 8h, InvokeAgentRuntime + runtimeSessionId.
Configuring the persistent mount path so filesystem state survives stop/resume of a session.
Per-protocol ports, paths, and the session header names (X-Amzn-Bedrock-AgentCore-Runtime-Session-Id vs Mcp-Session-Id).
Verify the live numbers: idle/max timeouts, 2 vCPU/8 GB ceiling, ~1 GB session storage, active-session quotas, ≥33-char session ID.
GA deep-dive that frames the per-session (Firecracker) microVM security boundary and isolation rationale.