This guide covers AppSync Bedrock with production-focused examples and trade-offs.
Exposing Bedrock through AppSync feels elegant: one GraphQL schema, Cognito auth, and a mutation that “just asks the assistant.” It is also how a single buggy client loops into a five-figure overnight bill. Production AppSync+Bedrock needs auth context propagation, strict input schemas, tool allowlists, and hard cost budgets — not a resolver that forwards raw strings toInvokeModel.
⚡ TL;DR: Keep GraphQL types narrow; put Bedrock calls behind Lambda resolvers that inherit Cognito sub/tenant claims; enforce per-tenant rate limits and token budgets in DynamoDB; validate tool arguments against JSON Schema before side effects. Stream via subscriptions only with abort and heartbeat. Illustrative SLO: p95 resolver setup < 300ms before first model token; hard deny when monthly tenant token budget exhausted.
Schema that cannot express unbounded spend — AppSync Bedrock
# schema.graphql — illustrative
type AskResult {
requestId: ID!
answer: String!
citations: [String!]!
inputTokens: Int!
outputTokens: Int!
}
input AskInput {
question: String! @length(max: 4000)
repoId: ID!
# ❌ Never: freeform modelId from the client
# ❌ Never: raw toolName + JSON blob
}
type Mutation {
askCodeAssistant(input: AskInput!): AskResult!
@aws_cognito_user_pools
@rateLimit(limit: 30, duration: 60) # illustrative directive / WAF pairing
}
modelId, temperature, and max tokens server-side. Clients choose product features, not foundation model SKUs.
Lambda resolver: auth → budget → Bedrock
// resolvers/askCodeAssistant.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, UpdateCommand } from "@aws-sdk/lib-dynamodb";
import {
BedrockRuntimeClient,
ConverseCommand,
} from "@aws-sdk/client-bedrock-runtime";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const bedrock = new BedrockRuntimeClient({});
const BUDGET_TABLE = process.env.BUDGET_TABLE!;
const MODEL_ID = process.env.MODEL_ID!; // pinned
const MAX_OUT = 1024;
export const handler = async (event: any) => {
const tenantId = event.identity?.claims?.["custom:tenant_id"];
const sub = event.identity?.sub;
if (!tenantId || !sub) throw new Error("Unauthorized");
const question: string = event.arguments.input.question;
const repoId: string = event.arguments.input.repoId;
if (!/^[\w-]{1,64}$/.test(repoId)) throw new Error("Invalid repoId");
// Atomic budget consume — fail closed
try {
await ddb.send(
new UpdateCommand({
TableName: BUDGET_TABLE,
Key: { pk: `TENANT#${tenantId}`, sk: "BUDGET#MONTH" },
UpdateExpression: "ADD tokensUsed :t SET updatedAt = :u",
ConditionExpression: "tokensUsed < maxTokens",
ExpressionAttributeValues: {
":t": 1500, // conservative pre-debit; reconcile with actual later
":u": new Date().toISOString(),
},
})
);
} catch {
throw new Error("Token budget exceeded");
}
const resp = await bedrock.send(
new ConverseCommand({
modelId: MODEL_ID,
system: [
{
text: `Tenant ${tenantId}. Answer only about repo ${repoId}. Refuse secrets.`,
},
],
messages: [{ role: "user", content: [{ text: question }] }],
inferenceConfig: { maxTokens: MAX_OUT, temperature: 0.2 },
// toolConfig only if tools are allowlisted below
})
);
const text =
resp.output?.message?.content?.map((c) => ("text" in c ? c.text : "")).join("") ??
"";
return {
requestId: event.request?.requestId,
answer: text,
citations: [],
inputTokens: resp.usage?.inputTokens ?? 0,
outputTokens: resp.usage?.outputTokens ?? 0,
};
};
bedrock:InvokeModel on the pinned model ARN, dynamodb:UpdateItem on the budget table, read-only access to the tenant’s KB if used — mirror Bedrock Agents guardrail posture.
Tools from GraphQL: never trust client tool JSON
If the assistant can call tools (create ticket, search code), define tools in the Lambda, not in the mutation input:const TOOLS = [
{
toolSpec: {
name: "search_repo",
description: "Search symbols in the caller's tenant repo",
inputSchema: {
json: {
type: "object",
properties: { query: { type: "string", maxLength: 200 } },
required: ["query"],
additionalProperties: false,
},
},
},
},
];
// When executing search_repo, force tenantId from Cognito claims — never from model args
❌ Let the model pass
tenantId as a tool argument.
Combine with Knowledge Base metadata filters for multi-tenant RAG (companion post in this wave).
Rate limits, WAF, and abortable streams
- API Gateway / AppSync: per-user throttling + AWS WAF rate rules
- DynamoDB token buckets for soft product limits (see cost-controls companion)
- For subscriptions/streaming: handle client disconnect; do not continue billing a full completion into the void
# CloudWatch alarm — illustrative
aws cloudwatch put-metric-alarm \
--alarm-name appsync-bedrock-tokens-high \
--metric-name InputTokenSum \
--namespace CheatCoders/AppSyncBedrock \
--threshold 5000000 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--period 3600 \
--statistic Sum \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ai-spend
Closing checklist
✅ Dos – ✅ Pin model ID and maxTokens server-side – ✅ Pre-debit or atomically check per-tenant budgets – ✅ Derive tenant from Cognito claims for tools and KB filters – ✅ Validate tool args with JSON SchemaadditionalProperties: false
– ✅ Alarm on token burn and error spikes
❌ Don’ts
– ❌ Don’t expose raw InvokeModel inputs on the GraphQL surface
– ❌ Don’t trust client-supplied model IDs or tool names
– ❌ Don’t skip auth on “internal” AppSync APIs
– ❌ Don’t let resolvers hold * IAM on Bedrock and DynamoDB
– ❌ Don’t stream forever after the subscriber vanished
Related reading
- Amazon Bedrock Agents: Tool Use, Memory, and Production Guardrails
- LLM Coding Agents on AWS: Safe Tool Sandboxes with Lambda
- Bedrock Prompt Caching and Batch Inference: Cut Latency and Cost
- AI Code Review Bots: IAM, Secrets, and Least-Privilege Pipelines
Related: Bedrock Action Groups: OpenAPI Tools That Typecheck Against Handlers · Bedrock Agents + EventBridge: Async Tools Without Blocking the Session · Bedrock Guardrails: Block Prompt Injection Inside Internal Dev Tools
Related: RAG vs Fine-Tuning vs Prompting: When to Use Each for AI-Powered Developer Tools · Day 56: Supervisor Patterns on Bedrock Agents · GraphQL vs REST: A Practical Comparison for Production APIs in 2025
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
