API Gateway WebSocket Fan-Out: Coalesce Pushes After Reconnect Storms

API Gateway WebSocket Fan-Out: Coalesce Pushes After Reconnect Storms

A 30-second network blip can bounce tens of thousands of sockets. Each reconnect triggers presence fan-out, missed-message catch-up, and auth refreshes — a thundering herd that looks like a DDoS against your own postToConnection path. Seniors coalesce pushes, jitter catch-up, and shed non-critical fan-out under pressure.

⚡ TL;DR: Buffer outbound events per connection with a short coalesce window; collapse duplicate topics. Jitter reconnect catch-up. Cap postToConnection concurrency and drop ephemeral presence under load. Pair with Consistent Hashing for WebSockets and Kafka to Lambda Backpressure.

Coalesce buffer per connection

// ws/coalesce.ts
type Outbound = { topic: string; payload: unknown; ts: number };

const buffers = new Map<string, Map<string, Outbound>>(); // connId → topic → last

export function enqueue(connId: string, msg: Outbound, windowMs = 50) {
  let topics = buffers.get(connId);
  if (!topics) {
    topics = new Map();
    buffers.set(connId, topics);
    setTimeout(() => flush(connId), windowMs); // ✅ one timer per conn
  }
  topics.set(msg.topic, msg); // collapse duplicates by topic
}

async function flush(connId: string) {
  const topics = buffers.get(connId);
  buffers.delete(connId);
  if (!topics) return;
  const batch = [...topics.values()];
  await apiGw.postToConnection({
    ConnectionId: connId,
    Data: JSON.stringify({ type: "batch", items: batch }),
  });
}

❌ Firing postToConnection once per upstream event during a reconnect storm — you will hit 429s and amplify retries.

Jittered catch-up after reconnect

// ws/catchup.ts
export async function onConnect(connId: string, userId: string) {
  const delay = 200 + Math.random() * 1800; // ✅ spread load
  await sleep(delay);
  const missed = await loadMissed(userId, since: lastAck(userId));
  // Cap catch-up size — rest goes to "refresh from snapshot"
  const slice = missed.slice(0, 100);
  for (const m of slice) enqueue(connId, m, 50);
  if (missed.length > 100) enqueue(connId, snapshotHint(userId), 0);
}

Fan-out backpressure

// ws/fanout.ts
import { Semaphore } from "./sem";

const postSem = new Semaphore(200); // global postToConnection concurrency

export async function fanout(connIds: string[], payload: Outbound) {
  const critical = payload.topic === "billing" || payload.topic === "security";
  if (!critical && postSem.pressure() > 0.8) {
    metrics.hit("FanoutShed");
    return; // ✅ shed presence/typing indicators
  }
  await Promise.all(
    connIds.map((id) =>
      postSem.run(() =>
        apiGw.postToConnection({ ConnectionId: id, Data: JSON.stringify(payload) })
          .catch((e) => {
            if (e.statusCode === 410) return registry.drop(id); // gone
            throw e;
          })
      )
    )
  );
}
Load signal Action
postToConnection 429 Increase coalesce window, lower concurrency
Reconnect rate spike Enable catch-up jitter + snapshot hints
Presence QPS high Shed ephemeral topics first
DLQ on push worker Stop retries of ephemeral; keep critical

Connection registry hygiene

Stale connections after a blip produce 410 Gone storms. Sweep aggressively and treat 410 as success for delete. Same lease mindset as Lease-Based ECS Leadership — expire what you cannot heartbeat.

Closing checklist

✅ Dos
– ✅ Coalesce by topic per connection with a short window
– ✅ Jitter reconnect catch-up
– ✅ Cap postToConnection concurrency
– ✅ Shed ephemeral fan-out under pressure
– ✅ Treat 410 as registry delete, not a hard error

❌ Don’ts
– ❌ Don’t push every presence tick during reconnect spikes
– ❌ Don’t replay unbounded missed-message histories
– ❌ Don’t retry ephemeral topics forever
– ❌ Don’t synchronize all clients on the same reconnect timer
– ❌ Don’t ignore 429s from API Gateway as “transient noise”

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply