launchthat
Real-Time Everything: Why We Stopped Polling and Never Went Back
Our trading dashboard polled every 5 seconds and users complained about stale data. We rebuilt on Convex with real-time subscriptions and the difference was not incremental — it was a different product.
The first time I saw a dashboard update without a page refresh, I thought it was a visual trick. The data just appeared. No spinner. No "last updated 5 seconds ago" timestamp. It was just there, current, every time I looked at it.
That was the moment I understood why polling is not good enough for anything that matters.

The problem with HTTP polling
Our trading dashboard started with a simple pattern:
- User loads page → server sends data
setIntervalfires every 5 seconds → fetch again- State updates → re-render
For a dashboard where prices change every 100ms, a 5-second poll interval means the data is always wrong. Users would see a price, click to trade, and the price had already moved. They complained about "lag" and "stale data" and they were right.
We tried reducing the interval to 1 second. The server started struggling under the request volume. We tried 2 seconds. Still too much load, still noticeably stale.
The fundamental problem is that polling asks the server "has anything changed?" thousands of times when nothing has, and misses the actual change by up to the full interval. It is both too expensive and too slow at the same time.
The solution: Convex real-time subscriptions
Convex is not just a database — it is a real-time execution engine. Instead of the client asking "has anything changed?", the server tells the client when something changes.
The old way:
const [data, setData] = useState([]);
useEffect(() => {
const interval = setInterval(() => {
fetch("/api/dashboard").then((r) => r.json()).then(setData);
}, 5000);
return () => clearInterval(interval);
}, []);
The new way:
const data = useQuery(api.dashboard.list);
That is the entire client-side data fetching code. When data changes on the server, Convex pushes the update to all subscribed clients. No websocket management. No invalidation logic. No loading state beyond the initial render.
Real-time mutations
Mutations are just functions that modify data. Every client watching the affected data sees the update in milliseconds:
export const updateWidget = mutation({
args: { widgetId: v.id("widgets"), position: v.number() },
handler: async ({ db }, { widgetId, position }) => {
const widget = await db.get(widgetId);
if (!widget) throw new Error("Widget not found");
await db.patch(widgetId, { position, updatedAt: Date.now() });
},
});
When this mutation commits, every client subscribed to a query that includes this widget sees the new position immediately. Not "within 5 seconds." Immediately.
Beyond the database: real-time infrastructure
We did not stop at data subscriptions. We built real-time infrastructure for everything.
Presence: who is online?
We built a WebSocket-based presence relay that tracks user connections across sessions. When you open a shared workspace, you see avatars of everyone currently viewing. When someone joins or leaves, the presence list updates instantly.
This is not a "nice to have." In a collaboration tool where an AI agent and a human are editing the same support response, knowing who is looking at what prevents conflicts and duplicate work.
Transport relay: persistent connections
For our bot platform, we needed persistent connections to cloud providers. We built a transport relay that maintains long-lived WebSocket connections, routes messages between apps and provider APIs, and handles reconnection and message queuing automatically.
The developer experience shift
Before Convex, building a real-time feature meant:
- Write REST endpoint
- Write frontend fetch logic with polling interval
- Handle race conditions when two polls return out of order
- Debug why the data is stale
- Add error handling for network failures
- Manage cleanup on component unmount
After Convex:
- Write a Convex query
- Use it in a component
- Done
The subscription lifecycle is automatic. Component unmounts, subscription closes. User loses connection, Convex reconnects and replays missed updates.
What real-time enables
Collaborative editing. Multiple users editing the same content see each other's changes live. We use this in our support plugin where AI and human agents collaborate on responses.
Live dashboards. Trading platforms, analytics dashboards, system monitors — all update without refresh. We built trading charts with lightweight-charts that receive price updates via Convex subscriptions.
Presence and notifications. "User is typing..." indicators, "3 people viewing this document" badges, new message counts that update the moment they arrive.
Workflow automation. When a Stripe webhook fires, the database updates. The CRM plugin sees that update in real-time and triggers follow-up actions. No cron jobs checking for changes every 5 minutes.
The tradeoffs
Real-time is not free:
- Infrastructure complexity — you need servers that maintain WebSocket connections, though Convex handles this
- Debugging is different — updates come from the server, not from user actions, so the mental model changes
- Over-engineering risk — not everything needs to be real-time, and making everything reactive can obscure simple business logic
But for the features that matter — collaboration, live data, instant feedback — the tradeoff is not even close. We stopped polling and the product became something fundamentally different. Users stopped complaining about stale data because the data was never stale again.
Want to see how this was built?
Try ConvexWant to see how this was built?
Browse all posts