I got an email from Vercel.
"You've used 75% of your free plan resources."
I ignored it.
A few hours later, I was at 100%.
I'd been building VisionBoard β AI workflows, MCPs, goal tracking, task assignments, a notification engine, the usual list of things a project management platform needs. Everything seemed to be working.
Then it wasn't.
I dug through the logs. One endpoint: /api/notifications/stream
.
I'd built the notification system with Server-Sent Events, running on a serverless function.
Here's the part I didn't think through: serverless functions aren't built to stay alive forever.
Vercel would kill the function on timeout. The browser would immediately reconnect. The function would spin back up. Then time out again. Then reconnect again.
An endless loop, quietly eating my plan limit while I wasn't looking.
The fix was almost embarrassingly simple β I ripped out the persistent SSE connection and replaced it with smart polling. Polling fits serverless. SSE doesn't, not without infrastructure built to keep a connection alive.
A while back I built a messaging app and went the opposite direction β plain polling, checking for new messages every few seconds.
It worked, technically. Except users had to reload the page or navigate away and back before a new message actually showed up. The polling interval existed, sure, but something about how I'd wired it meant the UI just... didn't refresh on its own. People sat there staring at a chat that looked dead until they hit refresh.
Two projects. Two opposite mistakes. Same root cause both times β I picked a real-time strategy based on what sounded right, not what actually matched the thing I was building.
Polling β you just ask repeatedly, "anything new?" Cheap to build, easy to reason about, but only as good as how you wire the refresh. Get the interval or the state update wrong and it feels broken even when it's technically working β which is exactly what happened with the messaging app.
SSE β one-way push from server to client, no reconnect logic to write yourself, the browser handles that. Great fit when you're on a long-running server. Terrible fit on serverless, because "long-running" is the one thing serverless isn't built for. I learned that by burning through a resource limit in one afternoon.
WebSockets β full duplex, both sides talking. I didn't reach for this on either project, and looking back, neither actually needed it. Notifications only flow one direction. Messages could've worked with SSE or polling done right. WebSockets would've just added a stateful connection I'd have had to babysit for no real benefit.
Webhooks β different problem entirely, this is server-to-server. Not something either of these two projects needed, but it's the one I reach for now when a third-party system needs to tell my backend something happened, instead of my backend polling them asking.
Just because something works locally doesn't mean it's production-friendly. SSE worked fine on my machine β no timeouts, no reconnect storm, because my machine doesn't kill idle processes every 10 seconds.
Real-time features have to match the infrastructure they're running on, not just the feature spec. "I need live updates" doesn't tell you which of these four to use. "I'm running on serverless" does. "Users need to talk back" does. The infra constraint is often the real answer, and I kept skipping past it to grab whatever sounded most real-time.
Sometimes the biggest fix isn't faster code. It's picking the right architecture in the first place.