Akshay Parkhi's Weblog

Subscribe

AWS Bedrock AgentCore Async Agents

27th February 2026

AWS Bedrock AgentCore lets you deploy AI agents as managed microVMs with built-in health checks, session management, and async task support. The async pattern is the interesting part — your agent responds immediately, runs work in the background, and the client polls for results. Here’s how the architecture works.

MicroVMs and Runtime Sessions

Each runtimeSessionId gets its own isolated microVM. This is the most important concept to understand:

WITHOUT runtimeSessionId:
─────────────────────────────────────────────────────────
Call 1 (no sessionId)  →  MicroVM #1 (random session)
Call 2 (no sessionId)  →  MicroVM #2 (random session)
Call 3 (no sessionId)  →  MicroVM #3 (random session)

Each call creates a NEW microVM. Tasks are isolated.


WITH runtimeSessionId:
─────────────────────────────────────────────────────────
Call 1 (sessionId="my-session")  ┐
Call 2 (sessionId="my-session")  ├─→  SAME MicroVM
Call 3 (sessionId="my-session")  ┘

All calls reuse the SAME microVM. Tasks share the instance.

The lifecycle timers control how long the microVM stays alive:

Default Settings:
├─ idleRuntimeSessionTimeout: 900s (15 minutes)
└─ maxLifetime: 28800s (8 hours)

Timeline for a 40-minute task:
─────────────────────────────────────────────────────────
00:00       Task starts (Status: BUSY)
00:00-40:00 MicroVM alive, doing work
40:00       Task completes (Status: Healthy)
40:00-55:00 Idle period (15 min countdown)
55:00       MicroVM terminates — ALL DATA LOST

idleRuntimeSessionTimeout resets on each invocation. maxLifetime starts at microVM creation and cannot be reset — it’s an absolute ceiling.

Synchronous Flow

A normal request-response cycle:

Client                  AgentCore Runtime              Your Agent
  │                            │                            │
  │  1. POST /invocations      │                            │
  │  {"prompt": "hello"}       │                            │
  ├───────────────────────────▶│                            │
  │                            │  2. Forward to agent       │
  │                            ├───────────────────────────▶│
  │                            │  3. Agent responds         │
  │                            │◀───────────────────────────┤
  │  4. Return response        │                            │
  │◀───────────────────────────┤                            │
  │  "Hello! How can I help?"  │                            │

Client waits for the full response. Fine for fast queries, but blocks on anything that takes more than a few seconds.

Asynchronous Flow

For long-running tasks, the agent responds immediately and processes in the background:

Client                  AgentCore Runtime              Your Agent
  │                            │                            │
  │  1. POST /invocations      │                            │
  │  {"prompt": "start task"}  │                            │
  ├───────────────────────────▶│                            │
  │                            │  2. Forward payload        │
  │                            ├───────────────────────────▶│
  │                            │  3. Agent:                 │
  │                            │     ├─▶ Spawn thread       │
  │                            │     └─▶ Task ID: 12345     │
  │                            │         Status: BUSY       │
  │                            │                            │
  │                            │  4. Immediate response     │
  │                            │◀───────────────────────────┤
  │  5. Return response        │     "Task started!"        │
  │◀───────────────────────────┤                            │
  │  "Task 12345 started"      │                            │
  │                            │                            │
  │                            │  ┌──────────────────────┐  │
  │                            │  │ BACKGROUND THREAD    │  │
  │                            │  │ ... doing work ...   │  │
  │                            │  │ complete_async_task()│  │
  │                            │  └──────────────────────┘  │

The client gets a task ID back instantly. The background thread runs for as long as needed — minutes, hours — and marks itself complete when done.

Health Check Monitoring

AgentCore pings your agent every 15 seconds to check status:

AgentCore Runtime              Your Agent
  │                            │
  │  GET /ping (every 15s)     │
  ├───────────────────────────▶│
  │  {"status": "HealthyBusy"} │  ← While task running
  │◀───────────────────────────┤
  │                            │
  │  GET /ping                 │
  ├───────────────────────────▶│
  │  {"status": "Healthy"}     │  ← After task completes
  │◀───────────────────────────┤

HealthyBusy tells AgentCore the microVM is alive and working — don’t kill it. Healthy means the work is done and the idle timeout countdown begins.

The Data Loss Problem

When the microVM terminates after its idle timeout, all in-memory data is lost. You must store results externally and have the client poll for completion:

Client                    AgentCore              Storage (DynamoDB/S3)
  │                            │                         │
  │ 1. Start task              │                         │
  ├───────────────────────────▶│                         │
  │ "Task 123 started"         │                         │
  │◀───────────────────────────┤                         │
  │                            │                         │
  │                            │  [Task runs 40 min]     │
  │                            │  Store result ─────────▶│
  │                            │                         │
  │ 2. Check result            │                         │
  ├───────────────────────────▶│                         │
  │                            │  Query storage ────────▶│
  │                            │◀─ "still running" ──────┤
  │◀───────────────────────────┤                         │
  │                            │                         │
  │ [Wait 5 minutes]           │                         │
  │                            │                         │
  │ 3. Check result again      │                         │
  ├───────────────────────────▶│                         │
  │                            │  Query storage ────────▶│
  │                            │◀─ "completed: data" ────┤
  │◀───────────────────────────┤                         │
  │ "Result: data"             │                         │

Storage options:

  • DynamoDB — best for structured results, queryable by task ID
  • S3 — best for large files and data blobs
  • ElastiCache/Redis — best for temporary results with TTL
  • In-memory — lost on termination, testing only

Agent Code

The agent uses two tools: one to start background work, one to poll for results.

app = BedrockAgentCoreApp()

@tool
def start_background_task(duration: int):
    task_id = app.add_async_task("processing")  # Status → BUSY

    def background_work():
        time.sleep(duration)  # Do actual work here

        # Store result externally — survives microVM termination
        result = f"Task completed after {duration}s"
        dynamodb.put_item(Item={'taskId': task_id, 'result': result})

        app.complete_async_task(task_id)  # Status → Healthy

    threading.Thread(target=background_work).start()
    return f"Task {task_id} started. Poll with get_task_result({task_id})"

@tool
def get_task_result(task_id: str):
    """Poll for task completion"""
    response = dynamodb.get_item(Key={'taskId': task_id})
    if 'Item' in response:
        return f"Result: {response['Item']['result']}"
    return f"Task {task_id} still running or not found"

@app.entrypoint
def main(payload):
    return agent(payload["prompt"])

add_async_task() marks the microVM as BUSY (prevents idle termination). complete_async_task() marks it as Healthy (idle countdown begins). Results go to DynamoDB so they survive microVM termination.

Deployment Pipeline

┌──────────────┐
│   Your Code  │
│ (Python file)│
└──────┬───────┘
       │ bedrock_agentcore_starter_toolkit.launch()
       ▼
┌──────────────────────────────────────────────────────┐
│  AWS CodeBuild                                       │
│  Builds Docker image with your agent code            │
└──────────────────────┬───────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────┐
│  Amazon ECR (Container Registry)                     │
│  Image: bedrock-agentcore-async_agent:20260227       │
└──────────────────────┬───────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────┐
│  AWS Bedrock AgentCore Runtime                       │
│  MicroVMs spin up per runtimeSessionId               │
│  ARN: arn:aws:bedrock-agentcore:us-east-1:...:runtime│
└──────────────────────┬───────────────────────────────┘
                       ▲
                       │ boto3.invoke_agent_runtime()
┌──────────────────────┴───────────────────────────────┐
│  Your Application / Client                           │
└──────────────────────────────────────────────────────┘

Client Code: With vs Without Session

With a session ID — all calls hit the same microVM:

client = boto3.client('bedrock-agentcore', region_name='us-east-1')
SESSION_ID = "my-session-123"

response = client.invoke_agent_runtime(
    agentRuntimeArn='arn:aws:bedrock-agentcore:...:runtime/async_agent',
    qualifier='DEFAULT',
    runtimeSessionId=SESSION_ID,  # Reuses same microVM
    payload=json.dumps({"prompt": "start a 5 second task"})
)

Without a session ID — every call creates a new microVM and tasks are completely isolated:

# Each call creates a NEW microVM
response = client.invoke_agent_runtime(
    agentRuntimeArn='arn:aws:bedrock-agentcore:...:runtime/async_agent',
    qualifier='DEFAULT',
    # No runtimeSessionId
    payload=json.dumps({"prompt": "start a 5 second task"})
)

Polling for Results

# Start task
response = client.invoke_agent_runtime(
    agentRuntimeArn=agent_arn,
    runtimeSessionId=SESSION_ID,
    payload=json.dumps({"prompt": "start a 600 second task"})
)
# Extract task_id from response

# Poll for completion
while True:
    time.sleep(60)
    result = client.invoke_agent_runtime(
        agentRuntimeArn=agent_arn,
        runtimeSessionId=SESSION_ID,
        payload=json.dumps({"prompt": f"get_task_result({task_id})"})
    )
    if "completed" in result:
        break

Lifecycle Patterns

PatternIdle TimeoutMax LifetimeNotes
Interactive chat10-15 min2-4 hoursBalance responsiveness with cost
Long-running tasks (40+ min)30 min8 hoursDynamoDB/S3 for results, client polling
Development/testing5 min30 minQuick cleanup for cost optimization

Monitoring

Tail the runtime logs:

aws logs tail /aws/bedrock-agentcore/runtimes/async_agent-65Z67nDn49-DEFAULT \
  --log-stream-name-prefix "2026/02/27/[runtime-logs]" --follow

Key Takeaways

  • Immediate response — agent returns a task ID instantly, doesn’t block the client
  • Background threads — work runs in a separate thread while the agent stays responsive to new invocations
  • Health checksHealthyBusy prevents premature termination during long tasks
  • External storage is mandatory — in-memory data is lost when the microVM terminates
  • Session ID is critical — without it, each call gets its own isolated microVM and cannot poll for results from a previous call
  • Two timers — idle timeout resets on each invocation; max lifetime is an absolute ceiling that never resets

Reference