DEV Community

alex
alex

Posted on

taming bursty websocket traffic with supabase edge functions

when you spin up a simple supabase edge function to proxy a websocket, it works fine for a few concurrent users. the moment a product launch or a live‑coding session pushes hundreds of connections within seconds, the function starts timing out, and you see 502s in the client console. this article walks through the root cause and a pragmatic pattern that keeps latency low without over‑engineering the stack.

why bursts break the naive setup

edge functions run in isolated containers that spin up on demand. each new container has a cold‑start penalty of ~50‑150 ms and a hard limit of 10 MB of inbound payload per request. a websocket handshake is just an HTTP upgrade, but every subsequent frame is still processed through the same request context. when dozens of connections arrive simultaneously, the platform throttles the number of active containers, queueing the upgrades. the result is dropped handshakes and jittery latency that quickly cascades into client‑side reconnect storms.

a lean edge‑function solution

instead of proxying every frame through the edge, keep the edge function solely as a connection broker. it validates the auth token, writes a short‑lived entry to supabase realtime, and returns a pre‑signed URL that points directly to a dedicated websocket server (e.g., a cheap node process on fly.io or a rust‑based tokio service). the client then connects straight to that server, bypassing the edge for the data path.

// supabase/edge/functions/connect.ts
import { serve } from "std/server";

serve(async (req) => {
  const auth = req.headers.get("authorization") ?? "";
  const user = await verifyJwt(auth);
  if (!user) return new Response("unauth", { status: 401 });

  // generate a signed URL valid for 30 seconds
  const wsUrl = `wss://ws.myservice.dev/${user.id}?exp=${Date.now()+30000}`;
  const signed = signUrl(wsUrl, process.env.SIGNING_KEY);
  return new Response(JSON.stringify({ url: signed }), {
    headers: { "content-type": "application/json" },
  });
});
Enter fullscreen mode Exit fullscreen mode

the edge function now does less than 5 ms of work per request, stays well within the 10 MB limit, and avoids any per‑frame processing.

tradeoffs & failure modes

  • cold‑start still matters – the first batch of connections will experience the spin‑up latency; mitigate with a cron “ping” that keeps a container warm during expected traffic windows.
  • signing key leakage – expose the signing key only to the edge runtime; rotate regularly and embed a short TTL in the URL to limit replay attacks.
  • backend scaling – the downstream websocket server must handle the burst; autoscaling on fly.io works but introduces its own cold‑start; consider a rust tokio binary with a low memory footprint to spin up quickly.
  • observability gap – you now have two hops (edge → broker → ws); instrument both sides with correlated request IDs to trace dropped frames.

the pattern shifts the heavy lifting off the edge platform, preserving its cheap request‑based billing model while still leveraging supabase auth.

what’s your approach to high‑throughput websocket bursts?

have you built a similar broker layer, or do you rely on a different platform altogether? share the edge cases you hit and how you mitigated them.

Top comments (0)