Skip to content

AppSync Events, part 1: a field guide to AWS's managed WebSockets

A verified picture of AppSync Events: what the service actually is, the complete pricing model, the quotas that shape designs, and the two open questions that started this series.

AWS AppSync WebSockets

Part 1 of a five-part series on AWS AppSync Events, built around Bid-Stream, a live-auction prototype designed to make the service’s hard parts unavoidable, then measure them. Part 2 covers the design, part 3 the build and test harness, part 4a the publisher-visible failure surface, and part 4b the load behavior.

AppSync Events fills a real gap in the serverless toolkit (managed WebSocket fan-out with no connection bookkeeping) behind a confusing name and documentation scattered across more pages than it deserves. When I went looking for the full picture, the best article I found answered a great deal and still left me with two questions that no amount of reading could settle. This series is about answering them the only way that works: build something that runs into them, and measure.

This first part is the reference piece: what the service actually is, the complete pricing model, the quotas that shape designs, and those two questions stated precisely. One method note that applies to the whole series: every factual claim is tagged, at least mentally, as documented, measured, or inferred, and the perishable ones carry dates.

What AppSync Events actually is

AppSync Events is managed WebSocket fan-out. Clients subscribe to channels over a WebSocket; publishers push JSON events to those channels over HTTP or WebSocket; the service broadcasts to every subscriber. There is no connection table to maintain, no $connect/$disconnect bookkeeping, none of the DynamoDB-full-of-connection-ids ritual that API Gateway WebSocket APIs require.

Channels are path-like names organized under namespaces: /auction/rare-book-42, /reactions/rare-book-42. The rules are stricter than they look, and they matter later: one to five slash-separated segments, each up to 50 characters, alphanumeric and dashes only (documented). Small fact, big design consequence: if a domain identifier rides in a channel name, that identifier’s format is now part of your API contract. Part 2 turns that constraint into a branded type.

Despite the name, this is not GraphQL. AppSync Events shares a console, a brand, and some internals with AppSync’s GraphQL APIs, and nothing else that matters to you. No schema, no resolvers in the GraphQL sense, no subscriptions-attached-to-mutations. It’s a pub/sub primitive.

What makes the service architecturally interesting arrived in April 2025: channel-namespace handlers. A namespace can route its publishes (and subscribe attempts) through either an APPSYNC_JS “code” handler or a direct Lambda integration, and the Lambda flavor has two invoke modes that could not be more different:

  • REQUEST_RESPONSE: synchronous. The handler sees every publish before broadcast and can transform events, reject them individually, or fail the batch. The handler is a gatekeeper: nothing broadcasts without its consent.
  • EVENT: asynchronous. The handler is invoked with the batch, and meanwhile, to quote the AWS docs because the sentence defines the mode, “AWS AppSync broadcasts the events as usual” (documented). The handler’s outcome never gates delivery. It is an observer, not a gatekeeper.

That one documented sentence carries more architectural weight than anything else on the page it appears on. It means EVENT mode gives you flat publisher latency and uncensorable broadcast, and it moves every failure-handling question into Lambda’s async machinery: retries, destinations, dead-letter queues. The whole back half of this series lives in the space that sentence opens.

REQUEST_RESPONSE · gates broadcast

broadcast · one data frame per accepted event

publisher · HTTP POST /event

AppSync Events API · namespace with Lambda handler

handler Lambda

subscribers · WebSocket

The article this series builds on

The best treatment of this service I’ve found is Ran Isenberg’s May 2025 piece, “Build Serverless WebSockets with AWS AppSync Events and Powertools for AWS Lambda” (that’s the author’s free mirror; the Medium copy is paywalled). It’s the starting point for this series and worth reading first. The mechanics are accurate: the namespace handler launch, code handlers versus direct Lambda, and both invoke modes correctly characterized. Choosing REQUEST_RESPONSE for authorization-sensitive paths is the right call, and the article says why. It flags that the Powertools resolver has no built-in payload validation (still true as of July 2026; validation is on you). Its infrastructure is more complete than most demos bother with: WAF association, field-level logging, a dedicated IAM role. And its bottom line, “confusing, poorly named, and not cheap, but very flexible,” is honest and, as the rest of this series bears out, about right.

An article has to stop somewhere, and where this one stopped is where my curiosity started. Three threads in particular pulled me in.

Three threads worth pulling

What is aggregate mode actually for?

The Powertools resolver processes publish batches in one of two modes: per-item (your handler is called once per event) or aggregate (your handler receives the whole batch). Aggregate mode gets little attention anywhere, and it’s easy to see why: for a handler that processes events independently, it’s strictly more code for the same result.

A natural first guess is that aggregate mode is a throughput lever: if per-item mode dispatched handlers serially, handling the batch yourself would save round trips. The source code retires that guess. The Powertools TypeScript resolver runs per-item handlers concurrently, a Promise.all over event.events.map (verified against v2.34.0). Per-item mode already has all the parallelism there is to have.

Which sharpens the question into an interesting one: if not throughput, what’s it for? The answer is cross-event logic that per-item mode cannot express. Every event in a publish batch arrives on the same channel. Five concurrent per-item handlers each see one event and can’t see their four siblings; an aggregate handler sees all five and can reason about them together: deduplicate within the batch, batch its writes, or exploit a domain invariant across them. Aggregate mode is a contention lever. Part 2 shows a domain where it wins outright, and I’ll leave the mechanism there.

The pricing model, in one place

The headline figure, $1.00 per million operations, is easy to find. The rest of the model takes assembling, and at fan-out scale the rest is most of the bill (verified July 2026 against the AppSync pricing page; re-verify before you budget):

  • $1.00 per million operations, where publishes, deliveries, handler invocations, and WebSocket operations all count. That last category is the one people miss: connects, subscribe requests, and pings are operations too, so a connection costs more than its connection-minutes.
  • Broadcast is billed per subscriber, metered per 5 KB delivered. One publish to 10,000 subscribers is 1 inbound operation plus 10,000 outbound operations: 10,001, not 1. At fan-out scale, outbound dominates everything, and fan-out is the entire point of the service.
  • $0.08 per million connection-minutes. 10,000 idle subscribers cost about $0.05 an hour in connection time alone, which is small until your subscriber count isn’t.
  • Free tier: 250k operations and 600k connection-minutes a month, for the first 12 months.

That third bullet carries an unresolved question I’d rather flag than paper over. If the periodic ka keepalives that part 3 measures on the wire bill as “ping requests,” then 10,000 idle subscribers generate roughly 600,000 operations an hour on top of their connection-minutes, and idle cost is an order of magnitude above the figure above. The pricing page names ping requests as operations without saying whether server-initiated keepalives are among them. Treat the $0.05 as a floor, not an estimate, until someone measures a billing period against a known idle fleet.

None of this is hidden; it’s just spread across pages that don’t cite each other, and the architecture decisions live in the assembled version.

The numbers that shape designs

AppSync Events is a quota-bounded service, and several of the bounds are fixed: designing without the table in front of you is designing blind. Here it is (AWS General Reference, verified July 2026; quotas drift, re-check):

QuotaValueAdjustable
Events per publish request5No
Inbound events per second per API10,000Yes
Request tokens per second (account, region)5,000; 10,000 in us-east-1 and other large regionsYes
Publish requests per WebSocket connection25/sNo
Outbound messages per second per API1,000,000Yes
Connect requests per second per API2,000Yes
Concurrent connections per APInone publishedn/a
Subscriptions per connection200Yes
Event payload size240 KBNo
Publish payload size (total)1.2 MBNo
Request execution time (bounds sync handlers)30 sNo

One note on the request-tokens row, because a stale figure circulates widely: the old default was 2,000, and AWS raised it in April 2024 to 5,000 in most regions and 10,000 in the larger ones, us-east-1 included. Applied values are per-account and visible in the Service Quotas console, so check yours rather than trusting either number.

Two of these quietly shape designs more than the rest. Five events per publish, fixed: your batch size is decided for you, and every batching strategy downstream inherits it. And 25 publish requests per second per WebSocket connection, fixed, which pushes any high-rate publisher off the WebSocket and onto the HTTP endpoint, where only the token and inbound-event quotas apply. If you’re building a load harness, you just learned why it publishes over HTTP.

The two open questions

Two questions survived all the reading. Both are central to running this service in production, and I couldn’t find either answered anywhere: not in the docs, not in the coverage.

Question one: partial batch handling, end to end. The Powertools docs will tell you that a per-item handler error becomes an error response for that item. Then the trail goes cold, because the full contract spans three parties. What must the handler return, exactly? What do subscribers see when two events in a batch of five are rejected? (Answer, documented but easy to miss: only returned events broadcast; rejected events vanish without a trace, and there is no error frame for them subscriber-side.) And the big one: what does the publisher see? AWS documents the publish request in detail and does not document the response body at all. The {successful, failed} ack shape comes from a tutorial and from the Amplify client’s source, whose PublishedEvent type is exactly {identifier, index}, no error field. Whether the handler’s carefully constructed error detail ever reaches the publisher is, as far as I can find, written down nowhere. A reliable publisher needs that answer.

Question two: backpressure and throttling. The quota table above bounds the front door, but the synchronous handler path couples AppSync to Lambda concurrency to a hot data store behind it, in any real design. When that chain saturates, what does the publisher observe? A status code? Which one? Per-event detail or a whole-batch failure? Retry guidance? None of it is documented. There’s a token-bucket metric (TokensConsumed) you can watch, and beyond that you’re inferring.

Both questions share a property: no amount of documentation-reading answers them. You have to build something that hits them and record what happens.

So we built something where both questions are unavoidable: a live auction, where a five-bid batch can carry three winners, one stale bid, and one malformed payload, a domain in which partial failure is the product working correctly. Where a bid must be accepted before it broadcasts, putting a Lambda and a hot DynamoDB item in the synchronous path on purpose. That design, and every constraint from this post that shaped it, is part 2.