Moving an agent to AgentCore Runtime V2 is a one-field config change, and that’s exactly why people will do it carelessly. The runtime now captures a snapshot after your container initializes and restores every new session from it. That’s how it gets roughly two-second P75 cold starts on images up to 2 GB. It’s also how a single careless line at module import quietly becomes shared state across every session you’ll ever serve. This is the migration playbook: audit init, tune loading for elastic memory, load test, canary, and keep a rollback path.
Rule one of AgentCore Runtime V2: init runs once, forever
On the classic runtime, every session booted your container and ran your init. On V2, init runs when the snapshot is created, and restores reuse the result. The platform versions documentation explains the lifecycle; the engineering consequence is that anything computed at import time is now computed once and cloned. If you’ve migrated functions to Lambda SnapStart, you’ve seen this movie.
Walk your agent’s startup path and flag anything in these buckets:
- Uniqueness. UUIDs, random seeds, nonces, or instance IDs generated at import. Every restored session gets the same value. Great for reproducibility, bad for anything that relies on uniqueness.
- Credentials and tokens. Secrets fetched from Secrets Manager, OAuth tokens, or STS credentials resolved at startup. They were valid when the snapshot was taken. They may not be valid when a session restores hours later, and every session shares them.
- Network connections. Database pools, websockets, and MCP client sessions opened at import. The socket on the other end doesn’t know it’s been cloned.
- Time. “Process start time” captured at import becomes snapshot time. Cache TTLs computed from it will be wrong.
- Per-user context. Anything that smells like tenant or user data has no business in module scope, snapshot or not.
The snapshot-safe pattern
The fix is boring: keep the expensive, deterministic work eager, and move anything unique or perishable behind a lazy accessor that runs inside the request. Here’s a before-and-after for a Strands Agents app:
import os, time, uuid
from functools import lru_cache
import boto3
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from tools import TOOL_REGISTRY
from prompts import SYSTEM_PROMPT
app = BedrockAgentCoreApp()
agent_template = dict(system_prompt=SYSTEM_PROMPT, tools=TOOL_REGISTRY)
@lru_cache(maxsize=1)
def secrets_client():
return boto3.client("secretsmanager")
_token = {"value": None, "expires": 0.0}
def api_token():
if time.time() > _token["expires"] - 60:
resp = secrets_client().get_secret_value(SecretId=os.environ["API_SECRET_ID"])
_token["value"] = resp["SecretString"]
_token["expires"] = time.time() + 900
return _token["value"]
@app.entrypoint
def invoke(payload, context):
request_id = str(uuid.uuid4())
agent = Agent(**agent_template)
result = agent(payload["prompt"])
return {"request_id": request_id, "result": str(result)}
if __name__ == "__main__":
app.run()
What moved where: imports, tool definitions, and the system prompt stay at module scope, because they’re deterministic and expensive, and that’s exactly what the snapshot should capture. The token lives behind an accessor that checks expiry on every call, so a restored instance fetches a fresh one on first use. The request ID is generated per invocation. The boto3 client is created lazily; creating clients at import is usually fine, but resolving credentials at snapshot time is the part you want to avoid.
Your tools call api_token() whenever they hit the downstream API, never a module-level constant. Building a fresh Agent per request also keeps conversation state from leaking between sessions through a shared object. If constructing the agent turns out to be measurably expensive, cache the pieces (model client, tool list), not the stateful agent itself. Then run the same audit on every third-party library that does work at import; some of them open connections or spawn threads without asking you first.

Eager vs. lazy under elastic memory
V2 changes the loading trade-off in two directions at once. Snapshots make eager init nearly free in latency terms, because you pay for it once. Elastic memory, on the other hand, bills for memory actually loaded, paged in on demand and reclaimed when the instance is cold, at a higher per-GB rate than V1. AWS’s guidance is that for most agents the bill still goes down, but your loading strategy decides where you land.
- Eager-load what every request touches. Framework imports, the tool registry, prompt templates. Those pages will be hot anyway.
- Lazy-load what only some requests touch. A large embedding model used by one rare tool, a big reference dataset, a PDF parser for the occasional upload. Under elastic memory, pages you never touch never get billed.
- Stop over-reserving. V1 habits like pre-allocating big buffers “just in case” now cost you on every session that doesn’t need them.
- Watch long sessions. An agent that accumulates conversation history or tool output in memory grows its footprint over time. Trim or offload it, for example to AgentCore Memory, rather than letting it balloon.
Check the current rates on the AgentCore pricing page and compare real sessions, not a spreadsheet guess.
Roll out with a versioned canary
Every update-agent-runtime call produces a new runtime version. Push the V2 change, then pin a named endpoint to the new version so only canary traffic hits it while your default callers keep using what they had:
aws bedrock-agentcore-control update-agent-runtime \
--region us-east-1 \
--agent-runtime-id support_agent-AbCdEf1234 \
--cli-input-json file://support-agent-v2.json
aws bedrock-agentcore-control create-agent-runtime-endpoint \
--region us-east-1 \
--agent-runtime-id support_agent-AbCdEf1234 \
--name canary \
--agent-runtime-version 7
The input file carries the same artifact, role, and network configuration as before, plus "platformVersion": "V2". Canary callers pass --qualifier canary to invoke-agent-runtime. Endpoint semantics, including how the default endpoint tracks versions, are covered in the AgentCore developer guide; read it before you assume which version your production traffic is on.
Rollback is a config change, not a rebuild: repoint the endpoint at the previous version, or push another update with the platform version set back. Keep the last known-good V1 version number in your runbook so nobody is guessing at 2 AM.
Load test before you trust it
Single-request timings tell you about cold starts. They tell you nothing about what happens when 200 sessions arrive at once. The amazon-bedrock-agentcore-samples repo includes load-testing material you can adapt; if you’d rather roll your own, a thread pool that fires fresh session IDs gets you most of the way:
import json, time, uuid
from concurrent.futures import ThreadPoolExecutor
import boto3
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
ARN = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/support_agent-AbCdEf1234"
def one(_):
t0 = time.perf_counter()
r = client.invoke_agent_runtime(
agentRuntimeArn=ARN, qualifier="canary",
runtimeSessionId=str(uuid.uuid4()),
payload=json.dumps({"prompt": "Status of order 1042?"}).encode())
r["response"].read()
return time.perf_counter() - t0
with ThreadPoolExecutor(max_workers=50) as pool:
lat = sorted(pool.map(one, range(500)))
print("p50", lat[len(lat) // 2], "p95", lat[int(len(lat) * 0.95)])
Check your account’s session and invocation quotas first so you’re testing the runtime, not the throttle. Run the same test against the old version for a baseline.
Observability that proves the migration worked
Define success before you flip anything: P75 first-response latency on new sessions, error rate, and runtime cost per thousand sessions. Pull latency and errors from Amazon CloudWatch and the AgentCore observability views, and compare V1 and V2 cohorts by endpoint. Log a snapshot-sensitive value, such as the token’s fetch time, on first use in each session; if you see identical values across sessions that should differ, your init audit missed something. Also confirm Region fit: V2 launched in us-east-1, us-east-2, us-west-2, eu-west-1, and ap-northeast-1, per the GA announcement.
Do the audit, run the canary for a week, and only then promote. The config change takes seconds; the discipline around it is what keeps you out of the postmortem.
At Enkompass (enkompass.net), this is our home turf. We build event-driven, serverless systems that scale to zero and bill like it, and we migrate agents with canaries and rollback plans, not crossed fingers. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.
