How to Add Real-time Features to Next.js Without Managing Servers
Learn how to add WebSocket-powered real-time features like live notifications, chat, and dashboards to your Next.js app deployed on Vercel — without running your own WebSocket server.
If you've ever tried to add real-time features to a Next.js app deployed on Vercel, you've probably hit a wall. WebSockets don't work with serverless functions. Vercel's own documentation confirms it: "Serverless Functions do not support WebSocket connections."
But your users expect real-time. They want to see notifications appear instantly, dashboards update without refreshing, and messages arrive the moment they're sent.
In this guide, I'll show you how to add real-time features to your Next.js app without spinning up a WebSocket server, without leaving Vercel, and without the complexity of enterprise solutions.
The Problem: Next.js + Vercel + WebSockets Don't Mix
Here's why real-time is hard on serverless:
-
Serverless functions are stateless — They spin up, execute, and terminate. There's no way to maintain a persistent WebSocket connection.
-
Vercel has execution limits — Functions timeout after 10-60 seconds depending on your plan. WebSocket connections need to stay open indefinitely.
-
No shared state — Each function invocation is isolated. You can't broadcast a message to "all connected users" because there's no concept of connected users at the function level.
The traditional solutions are:
- Polling — Wasteful, high latency, expensive at scale
- Self-hosted WebSocket server — Defeats the purpose of serverless
- Enterprise services — Overkill pricing for indie developers (see our Pusher and Ably comparisons)
There's a better way.
The Solution: Let Someone Else Hold the Connections
The key insight is this: you don't need to host WebSocket connections yourself. You just need to send messages to connected users.
This is exactly what PushFlo does:
- PushFlo maintains WebSocket connections with your users
- Your Next.js API routes send messages via simple HTTP POST requests
- Users receive updates instantly through their WebSocket connection
Your serverless functions stay serverless. Your users get real-time updates. Everyone wins.
Setting Up Real-time in Next.js (Step by Step)
Let's build a real-time notification system. When something happens in your app (new order, new message, status change), users will see it instantly.
Step 1: Install the SDK
npm install @pushflodev/sdk
Step 2: Create a PushFlo Account
Head to console.pushflo.dev and create a free account. You'll get:
- 500,000 messages/month
- 100 concurrent connections
- No credit card required
Grab your API key from the dashboard.
Step 3: Set Up Environment Variables
Create or update your .env.local:
PUSHFLO_API_KEY=your_api_key_here
NEXT_PUBLIC_PUSHFLO_APP_ID=your_app_id_here
Step 4: Create a Client-Side Hook
Create a reusable hook for subscribing to real-time updates:
// hooks/usePushFlo.ts
import { useEffect, useState } from 'react';
import { PushFloClient } from '@pushflodev/sdk';
export function usePushFlo(channel: string) {
const [messages, setMessages] = useState<any[]>([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const client = new PushFloClient({
publishKey: process.env.NEXT_PUBLIC_PUSHFLO_APP_ID!,
});
client.connect();
client.on('connected', () => {
setIsConnected(true);
});
client.subscribe(channel, {
onMessage: (message) => {
setMessages((prev) => [...prev, message]);
},
});
return () => {
client.disconnect();
};
}, [channel]);
return { messages, isConnected };
}
Step 5: Use the Hook in Your Components
// components/NotificationBell.tsx
'use client';
import { usePushFlo } from '@/hooks/usePushFlo';
export function NotificationBell({ userId }: { userId: string }) {
const { messages, isConnected } = usePushFlo(`user-${userId}`);
const unreadCount = messages.filter(m => !m.read).length;
return (
<div className="relative">
<BellIcon className="h-6 w-6" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
{unreadCount}
</span>
)}
{isConnected && <span className="absolute bottom-0 right-0 h-2 w-2 bg-green-500 rounded-full" />}
</div>
);
}
Step 6: Send Messages from Your API Routes
When something happens that should trigger a notification, send a message from your API route:
// app/api/orders/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const order = await request.json();
// Save the order to your database
const savedOrder = await db.orders.create(order);
// Send real-time notification to the user
await fetch('https://api.pushflo.dev/api/v1/publish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PUSHFLO_API_KEY}`,
},
body: JSON.stringify({
channel: `user-${order.userId}`,
event: 'new-order',
data: {
orderId: savedOrder.id,
message: 'Your order has been placed!',
timestamp: new Date().toISOString(),
},
}),
});
return NextResponse.json(savedOrder);
}
That's it. When an order is created, the user sees a notification instantly — no page refresh, no polling, no WebSocket server to manage.
Want a complete notification system? Check out Building Live Notifications in Under 10 Minutes for a step-by-step tutorial with a notification bell, toast popups, and server-side triggers.
Real-World Use Cases
This pattern works for any real-time feature:
Live Dashboard Updates
// When metrics change
await pushflo.publish('dashboard-metrics', {
activeUsers: 1234,
revenue: 56789,
}, { eventType: 'update' });
Chat Messages
// When a message is sent
await pushflo.publish(`chat-${roomId}`, {
sender: userId,
text: messageText,
}, { eventType: 'message' });
Collaborative Editing (Roll-Your-Own Presence)
// When a user starts editing
await pushflo.publish(`doc-${documentId}`, {
userId,
cursor: { x: 100, y: 200 },
}, { eventType: 'cursor-moved' });
Order Status Tracking
// When order status changes
await pushflo.publish(`order-${orderId}`, {
status: 'shipped',
trackingNumber: 'ABC123',
}, { eventType: 'status-update' });
Handling Missed Messages
What if a user's connection drops and they miss messages? PushFlo stores message history you can fetch on reconnect:
import { PushFloServer } from '@pushflodev/sdk/server';
const pushflo = new PushFloServer({ secretKey: process.env.PUSHFLO_SECRET_KEY! });
// Fetch recent messages for a channel (e.g. after a client reconnects)
const { messages } = await pushflo.getMessageHistory('dashboard-metrics', {
pageSize: 50,
});
The free tier keeps 1 day of history; Starter plan extends this to 3 days.
Performance Considerations
A few tips for production:
-
Use specific channels — Don't broadcast everything to everyone. Use user-specific channels (
user-123) or resource-specific channels (order-456). -
Keep payloads small — Payloads are capped per plan (4KB on Free, 16KB on Starter), and smaller messages = lower latency. Send IDs and let clients fetch full data if needed.
-
Debounce rapid updates — If you're sending many updates per second (like cursor positions), consider batching or throttling.
Cost Comparison
Let's compare the cost of different approaches for 10,000 daily active users:
| Approach | Monthly Cost | Complexity |
|---|---|---|
| Polling every 5 seconds | ~$200+ in API calls | Low |
| Self-hosted WebSocket server | $50-200+ (server costs) | High |
| Enterprise services (Pusher) | $49-299/month | Medium |
| PushFlo | $0-19/month | Low |
For most indie projects, the free tier is more than enough to get started and validate your idea.
Common Pitfalls to Avoid
-
Don't subscribe on every render — Use
useEffectwith proper dependencies and cleanup. -
Don't forget authentication — For sensitive data, validate user permissions before publishing to user-specific channels.
-
Don't ignore reconnection — The SDK handles this automatically, but make sure your UI reflects connection state.
Conclusion
Real-time features shouldn't require a PhD in distributed systems. With Next.js, Vercel, and PushFlo, you can:
- Keep your serverless architecture
- Add instant notifications, live updates, and real-time collaboration
- Deploy everything to Vercel with zero infrastructure to manage
- Start for free and scale as you grow
The pattern is simple: PushFlo holds the connections, you send the messages. Your users get the real-time experience they expect. For more details, see our guide on WebSockets for Vercel.
Ready to add real-time to your Next.js app? Get started with PushFlo for free — no credit card required.
Add real-time to your Next.js app in minutes
PushFlo works natively with Vercel — no servers, no config, just real-time.
Written by Marek
Indie developer building PushFlo — real-time messaging for serverless apps. Every post comes from running the thing in production. More about PushFlo →
Related Articles
Run Multiple Apps on One PushFlo Account with Projects
A step-by-step guide to splitting two apps into isolated PushFlo projects: scoped API keys, identical code, clean channel names, and per-app monitoring.
Introducing Projects: Organize Your Real-time Apps Without Changing a Line of Code
PushFlo now has projects — isolated namespaces for channels and API keys. Group apps and environments, monitor them separately, and switch by swapping one API key.
Do You Really Need Millions of Messages? The Real-time Pricing Trap
Why most real-time services sell you quotas you'll never use. A honest look at what indie developers actually need from WebSocket infrastructure.
