Browse documentation

Realtime in production (multi-instance)

SSE realtime works out of the box on a single Next.js instance. Multi-instance deployments (Coolify, Vercel, Fly, anywhere with >1 replica) need a Redis-backed publisher.

flowpanel ships two SSE drivers out of the box:

  • memory (default) — events live in the running Node process. Perfect for next dev, single-instance previews, single-container deployments.
  • redis — events fan out via Redis pub/sub. Required the moment you scale past one Next.js instance.

When you need Redis

The browser opens an EventSource connection to one instance. If a mutation lands on a different instance, the in-memory publishResource() fires on the wrong process and the subscribed browser never hears about it.

Symptoms: Realtime works locally but not in production. Lists don't refresh after a peer's edit. The dashboard's "Orders scraped today" counter doesn't tick up.

You need Redis any time all of these are true:

  • More than one Next.js instance answers requests for the same admin (Coolify with replicas > 1, Vercel with multiple regions, Fly with multi-region, Render with autoscale, etc.).
  • Sticky sessions are not configured (sticky sessions partially work around the issue but break under instance restarts).
  • Users have the admin open in browsers and you want their lists to update when a peer writes.

If you're just running next start on one box, the memory driver is fine.

Switch on Redis

// flowpanel.config.ts
import { defineAdmin } from "@flowpanel/kit";

export default defineAdmin({
  // …adapter, auth, resources…
  realtime: {
    driver: "redis",
    url: process.env.REDIS_URL!,
    // Optional: prefix every channel so multiple admins on the same Redis
    // instance don't collide. Defaults to "flowpanel".
    keyPrefix: "fp",
  },
});

That's the whole switch. Every existing resource.realtime: true, table({ realtime: true }), and publishResource(name, …) call now fans out via Redis instead of an in-process EventEmitter.

What flows over Redis

flowpanel never publishes row contents over Redis. Channels carry just resource name + action:

fp:resource:orders → { action: "update", id: "ord_…" }
fp:resource:users  → { action: "create", id: "usr_…" }
fp:custom:scraper-pipeline → arbitrary user payload via ctx.publish

Browsers subscribed to a list get the channel ping, then router.refresh() re-fetches the (RSC-streamed) updated rows over HTTPS — Redis is the notify, not the source of truth.

Operational checklist

  • Connection pooling. ioredis defaults are fine on small admins (under 1k concurrent SSE clients). At scale, share one publisher instance across the whole Next.js process — don't recreate per request.
  • TLS. Production Redis providers (Upstash, AWS ElastiCache, Render) use TLS. Pass tls: {} (object, not true) to ioredis options if your URL doesn't already encode it as rediss://….
  • HEAD requests / health checks. SSE responses are long-lived; ALBs default to 60s idle timeout. Bump the proxy/LB idle timeout to at least 5 minutes, or accept that browsers auto-reconnect every minute (works but spammy in logs).
  • Restart hygiene. Each Next.js instance opens one Redis subscriber connection. When you redeploy, in-flight EventSources disconnect and reconnect automatically — no data loss, but expect a thundering herd of reconnects right after rollout. Stagger your rolling deploy if you see Redis CPU spikes.

Troubleshooting

Symptom: Browser opens SSE, server sends events, browser receives nothing.

The proxy/CDN is buffering. SSE needs the response body to stream as each event lands; common breakage:

  • nginx: set proxy_buffering off; for the /api/flowpanel/stream location.
  • Cloudflare: enable "Streaming responses" or disable proxying on the stream route via a Page Rule.
  • Vercel: free tier closes Functions at 10s; stream needs runtime: "nodejs" + Pro tier (or self-host the stream route on a long-lived server).

Symptom: Browser receives events on instance A but not after a deploy that lands on instance B.

Almost always a publisher misconfiguration. The mutation route on instance A is fanning out in-memory only because either:

  1. realtime is unset (or { driver: "memory" }) in defineAdmin — it defaults to the in-memory driver, which can't cross instances, OR
  2. realtime.url points at a Redis that instance B can't reach, OR
  3. REDIS_URL env was set on A but not on B during a partial rollout.

flowpanel doctor (1.0.x) reports the active publisher driver and catches case 1 and 2 — case 3 needs a CI gate on env parity.