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.publishBrowsers 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, nottrue) to ioredis options if your URL doesn't already encode it asrediss://…. - 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/streamlocation. - 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:
realtimeis unset (or{ driver: "memory" }) indefineAdmin— it defaults to the in-memory driver, which can't cross instances, ORrealtime.urlpoints at a Redis that instance B can't reach, ORREDIS_URLenv 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.