Setting Up the Claude API in an AWS Lambda Function
Setting up the Claude API inside an AWS Lambda function means handling three things correctly: where the API key lives, how the Lambda runtime makes the outbound HTTPS request, and how you deal with Lambda's execution limits (timeout, payload size, and no persistent streaming). Get those three right and the rest is a normal Node.js or Python function.
This guide walks through a working Lambda setup end to end — packaging, secrets, a handler that calls Claude, and the deployment commands — plus the specific pitfalls that trip people up on serverless (cold starts, response size, and timeouts on long completions).
Prerequisites
- An AWS account with permissions to create Lambda functions, IAM roles, and (optionally) Secrets Manager secrets
- Node.js 18+ or Python 3.11+ runtime locally for packaging
- AWS CLI or AWS SAM CLI configured with credentials
- A Claude API key, either direct from Anthropic or an application key from a gateway like SubToAPI
Step 1: Store the API key securely
Never hardcode the key in your deployment package or set it as a plain environment variable in the console if you can avoid it. Use AWS Secrets Manager or SSM Parameter Store:
aws secretsmanager create-secret \
--name claude/api-key \
--secret-string '{"CLAUDE_API_KEY":"sk-ant-..."}'
Grant the Lambda execution role secretsmanager:GetSecretValue scoped to that specific secret ARN — not a wildcard.
Step 2: Write the handler
Here's a minimal Node.js handler that reads the key once per cold start and reuses it across warm invocations:
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
let cachedKey;
async function getApiKey() {
if (cachedKey) return cachedKey;
const client = new SecretsManagerClient({});
const res = await client.send(
new GetSecretValueCommand({ SecretId: "claude/api-key" })
);
cachedKey = JSON.parse(res.SecretString).CLAUDE_API_KEY;
return cachedKey;
}
exports.handler = async (event) => {
const apiKey = await getApiKey();
const body = JSON.parse(event.body || "{}");
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
messages: [{ role: "user", content: body.prompt }],
}),
});
const data = await response.json();
return {
statusCode: 200,
body: JSON.stringify(data),
};
};
Caching the key at module scope avoids a Secrets Manager call on every warm invocation — this matters because Secrets Manager calls add 50-150ms of latency you don't want on every request.
Step 3: Set a realistic timeout and memory
Lambda's default timeout is 3 seconds, which is far too short for an LLM call. Set:
aws lambda update-function-configuration \
--function-name claude-handler \
--timeout 60 \
--memory-size 512
For longer completions (large max_tokens, complex tool use chains), 60 seconds is a safer floor than 15–30. If you're fronting this with API Gateway, remember API Gateway REST APIs have a hard 29-second timeout regardless of your Lambda setting — use a Lambda Function URL or an ALB in front instead if you need longer.
Step 4: Deploy with SAM
A minimal template.yaml:
Resources:
ClaudeFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
Timeout: 60
MemorySize: 512
Policies:
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn: !Ref ClaudeApiKeySecret
FunctionUrlConfig:
AuthType: AWS_IAM
Deploy with:
sam build && sam deploy --guided
Using a Lambda Function URL instead of API Gateway avoids the 29-second cap and simplifies the setup if you don't need request transformation or custom domains out of the box.
Common pitfalls on Lambda specifically
- No native streaming support. Standard Lambda invocations buffer the full response before returning it to the client. If you need token-by-token streaming, you need Lambda response streaming (Function URLs with
InvokeMode: RESPONSE_STREAM) or you should run the streaming call from a long-lived service instead. - Cold starts add latency on top of Claude's response time. For latency-sensitive endpoints, provisioned concurrency helps, but it costs money even when idle.
- Payload limits. Lambda has a 6MB synchronous invocation payload limit. Large documents or long conversation histories can hit this before you hit any Claude-side limit.
- Retry storms. If your Lambda is triggered by SQS or EventBridge with retries enabled, a slow or failing Claude call can trigger duplicate invocations. Build idempotency into your handler.
Where a gateway simplifies this
A lot of the work above — key storage, retry logic, request logging — is boilerplate you end up rebuilding on every project. If you'd rather skip managing secrets rotation and raw x-api-key headers inside Lambda, SubToAPI gives you a generated sub_live_... key per application, so your Lambda function just calls https://api.subtoapi.app/v1/messages with a standard Authorization: Bearer header instead of handling Anthropic-specific headers and key storage yourself. It also gives you usage metadata per key, which is useful when a Lambda-based integration is shared across multiple services or teams. See the quickstart and messages endpoint docs for the exact request shape.
questions
Can I stream Claude responses from a standard Lambda function? Not with a normal synchronous invocation — the response is buffered. You need Lambda response streaming via a Function URL configured with RESPONSE_STREAM invoke mode, or run the call from a non-Lambda long-lived process. See streaming for how response streaming works at the API level.
Why does my Lambda time out even though I set a 60-second limit? If you're using API Gateway REST APIs in front of the function, that service enforces a hard 29-second cap independent of your Lambda configuration. Switch to a Lambda Function URL or an Application Load Balancer if you need longer execution windows.
Should I store the Claude API key as a Lambda environment variable or in Secrets Manager? Secrets Manager (or SSM Parameter Store) is safer — environment variables are visible to anyone with console/CLI read access to the function configuration and show up in CloudFormation templates and logs more easily than a scoped secret reference.