n4nAI

Serverless AI agents with AWS Lambda and Bedrock

A practical step-by-step guide to build and deploy serverless AI agents with AWS Lambda and Bedrock using SAM, including agent loop code and verification.

n4n Team3 min read588 words

Audio narration

Coming soon — every post will get a voice note here.

Serverless AI agents AWS Lambda Bedrock pair event-driven compute with managed model inference so you can ship an agent without provisioning a single server. This guide walks through a working deployment: a Lambda function triggered by S3 object creation that runs a reasoning loop against Bedrock, calls a DynamoDB tool, and persists the result. We use AWS SAM for reproducible infrastructure and Python 3.12 with boto3.

Step 1: Scaffold the SAM project

Create a working directory and initialize a minimal SAM app. I prefer the Python 3.12 runtime and the hello-world template as a base, then strip the boilerplate.

sam init --name bedrock-agent \
  --runtime python3.12 \
  --template hello-world \
  --app-template hello-world \
  --no-trace
cd bedrock-agent

Delete the default hello_world handler body and replace it with an agent package. Your layout should look like this:

bedrock-agent/
├── template.yaml
├── agent/
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
└── events/
    └── s3_put.json

In agent/requirements.txt, pin boto3 (already in Lambda runtime, but explicit is fine) and requests if you plan HTTP tools. For this walkthrough, only boto3 is needed.

Step 2: Define infrastructure in template.yaml

The SAM template declares the function, S3 trigger, DynamoDB table, and IAM permissions. Keep the function memory at 512 MB and timeout at 30 seconds; Bedrock calls are network-bound, not CPU-bound.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  AgentTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String

  BedrockAgentFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: agent/
      Handler: app.lambda_handler
      Runtime: python3.12
      MemorySize: 512
      Timeout: 30
      Environment:
        Variables:
          TABLE_NAME: !Ref AgentTable
          MODEL_ID: anthropic.claude-3-sonnet-20240229-v1:0
      Policies:
        - DynamoDBWritePolicy:
            TableName: !Ref AgentTable
        - Statement:
            - Effect: Allow
              Action: bedrock:InvokeModel
              Resource: "*"
      Events:
        S3Put:
          Type: S3
          Properties:
            Bucket: !Ref InputBucket
            Events: s3:ObjectCreated:*
  InputBucket:
    Type: AWS::S3::Bucket

Permissions note

The bedrock:InvokeModel action is account-scoped; restrict the Resource to the model ARN in production. The wildcard above is for brevity in the lab.

Step 3: Implement the agent loop

The handler reads the S3 event, fetches the object body as the user task, then runs a fixed two-step ReAct loop. For a real system, cap iterations to avoid runaway cost.

import json
import boto3
import os

s3 = boto3.client('s3')
ddb = boto3.resource('dynamodb').Table(os.environ['TABLE_NAME'])
bedrock = boto3.client('bedrock-runtime')
MODEL_ID = os.environ['MODEL_ID']

def invoke_bedrock(prompt: str) -> str:
    body = {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 512,
        "messages": [{"role": "user", "content": prompt}]
    }
    resp = bedrock.invoke_model(
        modelId=MODEL_ID,
        body=json.dumps(body)
    )
    data = json.loads(resp['body'].read())
    return data['content'][0]['text']

def lambda_handler(event, context):
    record = event['Records'][0]
    bucket = record['s3']['bucket']['name']
    key = record['s3']['object']['key']
    obj = s3.get_object(Bucket=bucket, Key=key)
    task = obj['Body'].read().decode('utf-8')

    sys = "You are an agent. To use a tool, reply with JSON: {\"action\":\"query\",\"id\":\"123\"}."
    first = invoke_bedrock(f"{sys}\nTask: {task}")
    if '"action":"query"' in first:
        # naive parse for demo
        id_val = first.split('"id":"')[1].split('"')[0]
        tool_result = ddb.get_item(Key={'id': id_val}).get('Item', {})
        final = invoke_bedrock(f"Tool returned {json.dumps(tool_result)}. Complete task.")
    else:
        final = first

    ddb.put_item(Item={'id': key, 'result': final[:4000]})
    return {"status": "ok", "key": key}

This code is intentionally minimal. In production, use a proper JSON parser and a loop guard.

Step 4: Add a DynamoDB tool

The table created in Step 2 is the tool surface. The agent reads an item by id to ground its response. Seed an item locally:

aws dynamodb put-item \
  --table-name BedrockAgentTable \
  --item '{"id":{"S":"123"},"name":{"S":"test-record"}}'

The ddb.get_item call in app.py already handles the lookup. If you need write tools, add a second action type and map it to ddb.put_item.

Step 5: Deploy with SAM

Build and deploy. Use a unique S3 bucket for artifacts; SAM prompts for it on first guided deploy.

sam build
sam deploy --guided

When asked for Stack Name, use bedrock-agent-stack. Accept defaults for region and confirmation. After completion, note the InputBucket name from the outputs.

Step 6: Verify end-to-end

Create a task file and upload it to the bucket. The event file in events/s3_put.json mirrors the Lambda event structure for local testing, but a real upload triggers the function.

echo "Summarize the record with id 123" > task.txt
aws s3 cp task.txt s3://<InputBucket>/

Check CloudWatch Logs for the function:

aws logs tail /aws/lambda/BedrockAgentFunction --follow

Success criteria: a new item appears in the DynamoDB table with the S3 object key as id and a non-empty result attribute.

aws dynamodb get-item \
  --table-name BedrockAgentTable \
  --key '{"id":{"S":"task.txt"}}'

If the item exists and result contains model-generated text referencing the seeded record, the pipeline works.

Cold starts and concurrency

Serverless AI agents AWS Lambda Bedrock pay a cold-start tax only on the first invocation per worker. Bedrock itself is a network call, so keep the function warm with a provisioned concurrency of 1 if latency matters. For bursty workloads, set ReservedConcurrentExecutions to avoid exhausting Bedrock account quotas.

Error handling and idempotency

S3 triggers are at-least-once. Make the handler idempotent by using the object key as the DynamoDB primary key and relying on put-item overwrite. Wrap invoke_bedrock in a retry with exponential backoff for ThrottlingException. Bedrock returns 429 when the region’s capacity is saturated; a short sleep and retry is the correct reaction, not a failure.

When to move off Lambda

If your agent loop needs more than 15 minutes or large memory (e.g., in-process vector search), Lambda is the wrong host. For most event-driven, stateless agents, though, serverless AI agents AWS Lambda Bedrock are the cheapest path to production. Use Step Functions when the reasoning graph becomes a DAG rather than a loop.

Tagsaws-lambdabedrockserverlessagent-deployment

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All agent deployment & hosting infrastructure posts →