n4nAI

Building a serverless chatbot with AWS Lambda and API Gateway

Step-by-step tutorial to deploy an aws lambda api gateway serverless chatbot that calls an LLM, with runnable code and expected outputs.

n4n Team3 min read631 words

Audio narration

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

Building an aws lambda api gateway serverless chatbot is the most cost-effective way to expose a conversational interface without running always-on infrastructure. This tutorial builds a working deployment from scratch: a Python Lambda that proxies a chat completion request to an OpenAI-compatible LLM endpoint, fronted by API Gateway’s HTTP API. You’ll get runnable code, deployment commands, and expected responses at each checkpoint.

Prerequisites

  • An AWS account with CLI v2 configured (aws configure) and a default region set.
  • Python 3.11 or newer locally for unit testing the handler logic.
  • An API key for an OpenAI-compatible inference gateway. We’ll use n4n.ai’s endpoint to get automatic fallback across 240+ models and per-token metering without custom retry code.
  • curl, jq, and zip available in your shell.
  • Familiarity with IAM roles and ARNs; replace 123456789012 and us-east-1 with your account details.

Step 1: Write the Lambda handler

Create app.py in an empty directory. The handler accepts API Gateway’s proxy event, parses the JSON body, and forwards the user message to the chat completions endpoint. We keep the dependency footprint zero—only the standard library—so the deployment package stays under 1 MB.

import os
import json
import urllib.request

ENDPOINT = os.environ.get("LLM_ENDPOINT", "https://api.n4n.ai/v1/chat/completions")
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini")

def handler(event, context):
    try:
        body = json.loads(event.get("body", "{}"))
        user_msg = body.get("message")
        if not user_msg:
            return _resp(400, {"error": "missing 'message' field"})
        
        payload = json.dumps({
            "model": MODEL,
            "messages": [{"role": "user", "content": user_msg}],
            "max_tokens": 256
        }).encode("utf-8")
        
        req = urllib.request.Request(ENDPOINT, data=payload, method="POST")
        req.add_header("Authorization", f"Bearer {API_KEY}")
        req.add_header("Content-Type", "application/json")
        
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode("utf-8"))
        
        reply = data["choices"][0]["message"]["content"]
        return _resp(200, {"reply": reply})
    except Exception as e:
        return _resp(500, {"error": str(e)})

def _resp(status, obj):
    return {
        "statusCode": status,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(obj)
    }

The code uses urllib to avoid packaging requests. API Gateway’s proxy integration expects a dict with statusCode, headers, and body (stringified). We return that shape directly. Cold starts are sub-second for this tiny package; if you later add heavy SDKs, expect longer init.

Step 2: Package and create the Lambda execution role

Lambda needs permission to write logs to CloudWatch. Create a trust policy and attach the managed basic execution policy.

cat > trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
EOF

aws iam create-role --role-name chatLambdaRole --assume-role-policy-document file://trust.json
aws iam attach-role-policy --role-name chatLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Zip the handler:

zip function.zip app.py

Create the function (replace account id and region):

aws lambda create-function \
  --function-name serverless-chatbot \
  --runtime python3.11 \
  --handler app.handler \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::123456789012:role/chatLambdaRole \
  --environment "Variables={LLM_API_KEY=sk-your-key,LLM_MODEL=gpt-4o-mini}"

Expected output includes FunctionArn and "LastUpdateStatus": "Successful". If you get InvalidParameterValueException on the zip, ensure you ran zip from inside the directory containing app.py.

Step 3: Wire API Gateway HTTP API

We use an HTTP API (not REST) for lower latency and simpler proxy setup. It also costs less per million requests.

API_ID=$(aws apigatewayv2 create-api \
  --name chat-api \
  --protocol-type HTTP \
  --query ApiId --output text)

INTEGRATION_ID=$(aws apigatewayv2 create-integration \
  --api-id $API_ID \
  --integration-type AWS_PROXY \
  --integration-uri arn:aws:lambda:us-east-1:123456789012:function:serverless-chatbot \
  --payload-format-version 2.0 \
  --query IntegrationId --output text)

aws apigatewayv2 create-route \
  --api-id $API_ID \
  --route-key "POST /chat" \
  --target "integrations/$INTEGRATION_ID"

aws apigatewayv2 create-stage \
  --api-id $API_ID \
  --stage-name prod \
  --auto-deploy

Grant API Gateway permission to invoke Lambda:

aws lambda add-permission \
  --function-name serverless-chatbot \
  --statement-id apigw-invoke \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:123456789012:$API_ID/*/*/chat"

The AWS_PROXY integration maps the incoming HTTP request directly to the Lambda event structure we parsed in Step 1.

Step 4: Test the aws lambda api gateway serverless chatbot

Fetch the endpoint URL:

URL=$(aws apigatewayv2 get-api --api-id $API_ID --query ApiEndpoint --output text)
curl -s -X POST "$URL/prod/chat" \
  -H "Content-Type: application/json" \
  -d '{"message":"What is the capital of Iceland?"}' | jq .

Expected success output:

{
  "reply": "The capital of Iceland is Reykjavík."
}

A malformed request returns:

{
  "error": "missing 'message' field"
}

A 500 with {"error": "HTTP Error 401: Unauthorized"} means the LLM_API_KEY is wrong. Because the n4n.ai gateway honors client routing directives, you could pin a specific provider by adding headers, but the default round-robin fallback already covers degraded providers.

Step 5: Harden timeouts and payload limits

API Gateway HTTP API has a 30-second integration timeout. Lambda’s timeout should be lower to fail fast. Update the function:

aws lambda update-function-configuration \
  --function-name serverless-chatbot \
  --timeout 25

Also, API Gateway limits payloads to 10 MB; our chatbot stays well under that. For production, add request validation in the handler (already done for missing message) and consider API keys at the gateway level using a Lambda authorizer. Set LLM_ENDPOINT via environment variable if you later swap gateways.

Step 6: Extending with conversation history

The current handler is stateless. To support multi-turn chat, store messages in DynamoDB keyed by a session_id passed in the body. Read prior messages, append the new one, call the LLM, then write back. This keeps the aws lambda api gateway serverless chatbot stateful without managing servers.

# Sketch only – requires boto3 and a table
import boto3
dynamo = boto3.resource("dynamodb").Table("chat_sessions")

def get_history(session_id):
    return dynamo.get_item(Key={"id": session_id}).get("Item", {}).get("messages", [])

def save_history(session_id, messages):
    dynamo.put_item(Item={"id": session_id, "messages": messages})

Add session_id to the request body and replace the static messages list with the retrieved array plus the new user turn.

Observability and cost notes

CloudWatch Logs will show each invocation. Because the LLM gateway meters per token, you can track spend by extending the handler to log the usage field from the response. The n4n.ai endpoint forwards provider cache-control hints, so repeated system prompts can be cached at the provider level if you set cache_control in the request—but we kept the example minimal.

Cleanup

Avoid stray charges:

aws apigatewayv2 delete-api --api-id $API_ID
aws lambda delete-function --function-name serverless-chatbot
aws iam detach-role-policy --role-name chatLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name chatLambdaRole

That’s a complete, deployable aws lambda api gateway serverless chatbot. Modify the prompt shape or add conversation history by extending the messages array before the LLM call.

Tagsaws-lambdaapi-gatewaychatbotserverless

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 aws lambda serverless llm integration posts →