Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Real-Time Notifications in Your Next.js SaaS -- Server-Sent Events Without a WebSocket Server

September 7, 2026
nextjssaasreal-timevercelboilerplate

The polling problem every growing SaaS hits

You launch your notification system with a 5-second polling interval. It works. Then you get 500 users and your database is fielding 6,000 notification queries per hour from users who are not even actively looking at their screen. You consider WebSockets -- then realize Vercel's serverless functions cannot keep a persistent connection alive between invocations.

There is a better option sitting inside the web platform that most developers skip: Server-Sent Events (SSE).

What Server-Sent Events actually are

SSE is a browser standard for one-way real-time data streaming over a regular HTTP connection. The browser opens one long-lived connection and the server pushes updates as they happen. No WebSocket library. No separate real-time server. No sticky sessions.

For most SaaS notification use cases -- "you have a new message", "your report is ready", "someone commented on your post" -- one-way is exactly what you need. The user does not send updates back through that channel.

SSE works on Vercel because it is just an HTTP response with a special content type and a stream that stays open. The browser connection remains alive through Vercel's edge network, and cleanup happens when the client disconnects.

Three use cases where SSE beats polling

Notification badges. Instead of hitting /api/notifications/unread-count every 5 seconds from every active tab, the browser opens a single SSE stream. Your server pushes a new count whenever a notification is created.

Background job status. A user triggers a CSV export or data sync. Instead of showing a spinner and polling /api/jobs/[id]/status, you stream progress updates back so they see real progress in real time.

Collaborative activity feeds. When one team member creates a record, other members in the same organization see it appear without a manual refresh.

All three are one-way flows from server to browser -- exactly what SSE was designed for.

How to wire it up in Next.js App Router

An SSE endpoint is a route handler that returns a ReadableStream with content type text/event-stream.

// app/api/notifications/stream/route.ts
export async function GET(req: Request) {
  const user = await getUserFromRequest(req);
 
  const stream = new ReadableStream({
    start(controller) {
      const send = (data: object) => {
        controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
      };
 
      notificationService.getUnreadCount(user.id).then(count => {
        send({ unreadCount: count });
      });
 
      let last = -1;
      const interval = setInterval(async () => {
        const count = await notificationService.getUnreadCount(user.id);
        if (count !== last) {
          last = count;
          send({ unreadCount: count });
        }
      }, 3000);
 
      req.signal.addEventListener("abort", () => {
        clearInterval(interval);
        controller.close();
      });
    }
  });
 
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    }
  });
}

On the client side, the native EventSource API opens and maintains the connection automatically:

// In your notification hook
useEffect(() => {
  const es = new EventSource("/api/notifications/stream", {
    withCredentials: true
  });
  es.onmessage = (e) => {
    const data = JSON.parse(e.data);
    setUnreadCount(data.unreadCount);
  };
  return () => es.close();
}, []);

The browser handles reconnection automatically if the connection drops. You do not need to write retry logic.

The trade-off you should understand

SSE still requires the server to do periodic DB queries inside the stream -- it is not magic. What changes is that 500 users with 3-second polling means your server queries the DB 3 times per second per user regardless of whether anything changed. With SSE, you check every 3 seconds but only push when the count changes, so the browser receives far fewer updates and there is no round-trip cost from 500 separate HTTP requests.

The bigger win is connection efficiency. Each browser tab that polls independently sends a full HTTP request with headers and auth tokens on every cycle. An SSE stream is one connection per tab, and the overhead after that is minimal.

For truly high-frequency updates -- live cursors, collaborative text editing, multiplayer state -- you want a dedicated real-time service like Pusher or Ably. SSE is the right tool when updates happen occasionally and "instant" means under 3 seconds.

Decision framework: polling vs SSE vs WebSockets

Use polling when updates are infrequent (less than once a minute) and the user can tolerate a few seconds of lag. It is the simplest option and easy to reason about.

Use SSE when you need updates within 1-3 seconds, the flow is server-to-browser, and you are on a serverless platform like Vercel. Most SaaS notification use cases land here.

Use WebSockets when you need bi-directional real-time communication at very low latency -- live chat, live cursors, collaborative editing. This requires either a separate persistent service or a provider like Pusher.

What the boilerplate already gives you

The Next.js SaaS Boilerplate ships a full in-app notification system with a Drizzle ORM schema, an unread count service, and mark-as-read endpoints. Adding an SSE stream on top is a few dozen lines -- you are connecting a new delivery mechanism to logic that already exists, not rebuilding notifications from scratch.

The existing background jobs pattern shows how the boilerplate handles Vercel's serverless constraints for cron work -- the same constraints that make SSE a better fit than WebSockets for real-time updates.

The auth helper (getUserFromRequest) and error handling (HttpError) work exactly the same inside a streaming route as in a regular route handler.

Ready to ship a SaaS that feels live without the WebSocket infrastructure? Get the boilerplate and add real-time notifications in a day.