Your Dashboard Is Lying to You (And Polling Is Why)
Every five seconds, your frontend fires a request. The server wakes up, queries the database, serializes the response, and sends it back. Most of the time, nothing has changed. You just burned CPU cycles, network bandwidth, and a user's patience to confirm that the world is exactly the same as it was five seconds ago.
This is polling. It's the duct tape of real-time UX, and it's time to pull it off.
The Actual Problem With Polling
The UX issue is obvious: a dashboard that refreshes on an interval never feels live. There's always a lag window where stale data sits on screen while something critical already happened in the database. But the less-discussed problem is the compounding server load. Fifty users polling every five seconds is 600 requests per minute just to check if anything is new. Scale that up and you're paying for infrastructure to serve "nothing changed" messages at volume.
WebSockets flip this model. Instead of clients asking "anything new?", the server says "something just happened" the instant it does. One persistent connection per client, zero wasted round trips.
Postgres + FastAPI: Lighter Than You Think
You don't need Kafka, RabbitMQ, or a managed pub/sub layer to get started. If you're already on Postgres, you have LISTEN/NOTIFY built in, and FastAPI has native WebSocket support. The plumbing is surprisingly minimal.
Here's the core pattern:
import asyncio
import asyncpg
from fastapi import FastAPI, WebSocket
app = FastAPI()
async def listen_for_updates(websocket: WebSocket):
conn = await asyncpg.connect("postgresql://user:password@localhost/db")
await conn.execute("LISTEN data_updates")
try:
while True:
notification = await conn.connection.notifies.get()
await websocket.send_text(notification.payload)
finally:
await conn.close()
@app.websocket("/ws/dashboard")
async def dashboard_ws(websocket: WebSocket):
await websocket.accept()
await listen_for_updates(websocket)
On the database side, a trigger fires NOTIFY data_updates, payload whenever a row changes. FastAPI picks it up through asyncpg and pushes it straight to the connected client. No polling loop, no scheduled jobs.
This works well for a single backend instance. It's straightforward to reason about, easy to debug, and has almost no operational overhead.
When You Need to Scale Out
Single-instance setups hit a wall when you add horizontal scaling. If you have three FastAPI pods and a notification fires on pod one, the clients connected to pods two and three never see it.
The fix is a shared broadcast layer. Redis pub/sub sits between your backend instances and ensures every pod hears every event. You pair this with a connection manager that tracks active WebSocket connections per instance and fans out incoming messages to all of them.
import aioredis
from fastapi import WebSocket
from typing import Set
class ConnectionManager:
def __init__(self):
self.active: Set[WebSocket] = set()
async def connect(self, ws: WebSocket):
await ws.accept()
self.active.add(ws)
def disconnect(self, ws: WebSocket):
self.active.discard(ws)
async def broadcast(self, message: str):
for ws in list(self.active):
await ws.send_text(message)
manager = ConnectionManager()
async def redis_listener():
redis = await aioredis.create_redis_pool("redis://localhost")
channel, = await redis.subscribe("data_updates")
async for message in channel.iter():
await manager.broadcast(message.decode())
Run redis_listener as a background task on startup and each pod independently subscribes to the Redis channel. Postgres notifies one pod, Redis fans it to all of them, each pod pushes to its own connected clients. Clean separation, easy to reason about.
On the frontend, add auto-reconnect logic. WebSocket connections drop. Network hiccups happen. A simple exponential backoff reconnect loop on the client side means users never notice.
Where This Pattern Has Limits
This architecture handles a lot. But as event volume grows, as you add filtering logic, per-user subscriptions, delivery guarantees, or backpressure handling, the custom plumbing starts to accumulate. You end up maintaining infrastructure instead of shipping features.
That's the problem Turboline was built to solve. The event pipeline, fan-out, backpressure, and scaling layer are handled for you, so teams can wire up the WebSocket endpoint and focus on what the dashboard actually shows rather than keeping the pipes from leaking.
The Concrete Takeaway
Start with Postgres LISTEN/NOTIFY and FastAPI WebSockets. It covers the majority of real-time dashboard use cases with almost no added complexity. Add Redis pub/sub when you scale horizontally. Only reach for a full streaming infrastructure layer when the operational cost of managing it yourself starts eating into shipping time.
Stop confirming that nothing changed. Start sending updates when something does.
Top comments (0)