Your coding-agent UI should paint “lint started → 12 files → diff ready” as it happens — not after the next GET /status lucks into a new row. AWS AppSync GraphQL subscriptions give you managed WebSocket fan-out with a GraphQL schema, auth modes (Cognito / IAM / OIDC / API key for demos only), and resolvers that publish when mutations land. Distinct from API Gateway WebSockets for multi-turn agents (you own connection tables and fan-out) and Lambda response streaming (one HTTP response, many chunks): AppSync subscriptions are long-lived push channels for progress events across tabs and clients.
⚡ TL;DR: Model
AgentProgressevents in GraphQL, mutate from the worker (Step Functions / Lambda / Fargate), subscribe from the UI with Cognito JWT, filter byrunId/tenantId, and keep the token stream itself on Lambda streaming or Bedrock ConverseStream — subscriptions carry progress, not every token. Related: EventBridge Pipes, App Runner HTTP APIs, DynamoDB Streams session hooks, AppSync + Bedrock resolvers.
Polling vs push for agent UX
| Pattern | Latency feel | Ops | Best for |
|---|---|---|---|
| Client poll REST | Jittery; wasted GETs | Simple | Tiny internal tools |
| Lambda response streaming | Great TTFB for one call | Function URL / invoke stream | Token chunks of a single turn |
| API GW WebSockets | Full control | Connection DynamoDB, fan-out code | Custom protocols, bidirectional cmds |
| AppSync subscriptions | Push with schema | Managed WS + auth | Progress events + typed GraphQL clients |
❌ Embedding a 60-second setInterval that hits DynamoDB for every open review tab.
Schema: progress events, not the whole transcript
# ✅ keep payloads small — UI can refetch heavy diffs via query
enum ProgressPhase {
PLANNED
TOOL_STARTED
TOOL_FINISHED
DIFF_READY
FAILED
COMPLETED
}
type AgentProgress @aws_cognito_user_pools @aws_iam {
runId: ID!
tenantId: ID!
phase: ProgressPhase!
toolName: String
message: String
percent: Int
artifactKey: String
ts: AWSDateTime!
}
type Mutation {
publishAgentProgress(input: PublishProgressInput!): AgentProgress
@aws_iam
}
type Subscription {
onAgentProgress(runId: ID!): AgentProgress
@aws_subscribe(mutations: ["publishAgentProgress"])
}
input PublishProgressInput {
runId: ID!
tenantId: ID!
phase: ProgressPhase!
toolName: String
message: String
percent: Int
artifactKey: String
}
Authorize so a Cognito user can subscribe only to their tenant’s runId. The worker publishes with IAM (@aws_iam on the mutation). Never leave API_KEY on for production agent progress.
Publish from the agent worker
# ✅ Lambda / Fargate worker — IAM auth to AppSync
import json, os, datetime
from urllib import request
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
APPSYNC = os.environ["APPSYNC_URL"] # https://xxxx.appsync-api.region.amazonaws.com/graphql
MUTATION = """
mutation Publish($input: PublishProgressInput!) {
publishAgentProgress(input: $input) {
runId phase toolName message percent ts
}
}
"""
def publish_progress(run_id: str, tenant_id: str, phase: str, **extra):
payload = {
"query": MUTATION,
"variables": {
"input": {
"runId": run_id,
"tenantId": tenant_id,
"phase": phase,
"ts": datetime.datetime.utcnow().isoformat() + "Z",
**{k: v for k, v in extra.items() if v is not None},
}
},
}
body = json.dumps(payload).encode()
aws_req = AWSRequest(method="POST", url=APPSYNC, data=body, headers={"Content-Type": "application/json"})
SigV4Auth(boto3.Session().get_credentials(), "appsync", os.environ["AWS_REGION"]).add_auth(aws_req)
# ❌ publishing without SigV4 / with a shared API key in the sandbox image
http_req = request.Request(APPSYNC, data=body, headers=dict(aws_req.headers), method="POST")
with request.urlopen(http_req, timeout=10) as resp:
out = json.loads(resp.read().decode())
if out.get("errors"):
raise RuntimeError(out["errors"])
return out["data"]["publishAgentProgress"]
Hook publishes at phase boundaries inside Step Functions or after each tool in your runner. For EventBridge-sourced tool completions, Pipes → Lambda → publishAgentProgress keeps the worker thin.
UI subscribe (Amplify / AppSync client)
// ✅ React — subscribe filtered by runId; Cognito JWT via Amplify
import { generateClient } from "aws-amplify/api";
const client = generateClient();
const sub = client
.graphql({
query: /* GraphQL */ `
subscription OnProgress($runId: ID!) {
onAgentProgress(runId: $runId) {
phase toolName message percent artifactKey ts
}
}
`,
variables: { runId },
})
.subscribe({
next: ({ data }) => {
const ev = data?.onAgentProgress;
if (!ev) return;
// paint timeline; fetch artifact from S3 only when DIFF_READY
setEvents((prev) => [...prev, ev]);
},
error: (err) => console.error("subscription", err),
});
// cleanup on unmount / run complete
// sub.unsubscribe();
Pair with Lambda response streaming for the token channel of the active turn: subscriptions = progress bus, streaming = token firehose. Mixing every token into AppSync will thrash connection quotas and cost.
AuthZ filters that actually matter
#if($context.identity.claims.get("tenant_id") != $context.args.input.tenantId)
$util.unauthorized()
#end
Also enforce on subscriptions via enhanced filtering / resolver checks so users cannot subscribe to arbitrary runIds. Log denials; feed suspicious patterns to GuardDuty / Security Hub workflows.
Limits and failure modes
| Issue | Symptom | Mitigation |
|---|---|---|
| Payload too large | Mutation rejected / slow UI | Store diffs in S3; send artifactKey only |
| Quiet periods | Client thinks job died | Heartbeat message every N seconds or phase ping |
| Fan-out storms | One run, thousands of tabs | Soft-cap concurrent subs; disconnect idle |
| Dual auth confusion | Worker 401 | IAM for publish, Cognito for subscribe — document both |
| Token spam on AppSync | Cost + lag | Tokens → stream; progress → subscription |
# ✅ CloudWatch: AppSync 4xx/5xx and connection counts
aws cloudwatch put-metric-alarm \
--alarm-name appsync-agent-progress-5xx \
--namespace AWS/AppSync \
--metric-name 5XXError \
--dimensions Name=GraphQLAPIId,Value=YOUR_API_ID \
--statistic Sum --period 60 --threshold 5 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1
Production checklist
- [ ] Schema separates progress mutations from heavy queries
- [ ] Cognito (or OIDC) for clients; IAM for workers; no prod API keys
- [ ] Subscription filter by
runId+ tenant authorization - [ ] Diffs/artifacts in S3 (optionally Express One Zone for hot scratch)
- [ ] Heartbeats or phase pings so UIs do not soft-deadlock
- [ ] Tokens stay on ConverseStream / Lambda streaming — not AppSync
- [ ] WAF / rate limits on the AppSync API endpoint
- [ ] Trace publish latency with Application Signals
- [ ] Document ownership tag
workload=coding-agent
FAQ
Q: Can AppSync replace WebSockets entirely?
A: For typed progress and data sync, yes for many products. If you need arbitrary binary frames, server-initiated cancel races, or a non-GraphQL protocol, keep API Gateway WebSockets or App Runner.
Q: Where do I put the LLM tokens?
A: Stream them on the request path (Lambda response streaming). Use AppSync for plan/tool/diff milestones and multi-viewer fan-out.
Q: DynamoDB + Streams instead of mutation publish?
A: Possible (Streams → Lambda → noneNone? → actually Streams → Lambda that calls the mutation, or use AppSync DynamoDB resolvers). Mutations remain the clearest “this event is now public to subscribers” boundary.
Related reading
- API Gateway WebSockets for Multi-Turn Coding Agents
- AWS Lambda Response Streaming: Stream Coding-Agent Tokens
- Amazon EventBridge Pipes: Wire Coding-Agent Tool Runners
- Appsync Bedrock: GraphQL Resolvers That Call Tools Safely
Push the progress. Stop polling hope.
Last updated on October 3, 2026
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
- Java Virtual Threads vs Traditional Threads: What Nobody Tells You
- Distributed Locks Reality Check: When Redis Redlock Is the Wrong Tool
Newly added
- Amazon Bedrock Custom Model Import: Host Fine-Tuned Coding Models Inside Your Account
- AWS Systems Manager Session Manager: Audited Break-Glass Shell into Coding-Agent Sandboxes
- Amazon S3 Express One Zone: Sub-Millisecond Scratch for Coding-Agent Tool Artifacts
- AWS AppSync GraphQL Subscriptions: Push Live Coding-Agent Progress Without Polling
- Amazon Neptune: Graph Memory for Code Dependency Reasoning in Coding Agents
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.