nextjs · react · sse · realtime

Update Next.js UI on Background Job Done Without Pusher

Skip fragile WebSockets and expensive polling loops. How to notify your Next.js React UI when background tasks complete using Server-Sent Events.

A lightweight Server-Sent Events pipeline streaming background job completion updates to a Next.js interface
A lightweight Server-Sent Events pipeline streaming background job completion updates to a Next.js interface · Photo: BoringPush

Your background worker just finished generating an AI report or processing an image upload.

Now comes the user experience dilemma: how do you let the browser know the job is done?

If you ask modern tutorials, they will tell you to install Socket.io and run a standalone WebSocket server. Or they will tell you to enter your credit card on Pusher or Ably.

If you don't want another monthly bill, developers fall back to aggressive polling: firing a fetch request every five hundred milliseconds to ask, "Is it done yet?"

Both approaches create unnecessary pain.

The problem with WebSockets in Next.js

WebSockets are designed for bidirectional, low-latency communication (like multiplayer gaming or real-time collaborative whiteboards).

For background job notifications, you do not need bidirectional communication. The client does not need to send messages to the server over the socket; it only needs to receive a one-way notification when a job completes.

Using WebSockets for one-way notifications introduces three major headaches:

First, protocol overhead. WebSockets require upgrading the HTTP connection to a persistent TCP stream. This bypasses standard HTTP caching and does not multiplex cleanly across HTTP/2 or HTTP/3 streams.

Second, mobile reliability. When a user switches apps or locks their phone screen, mobile Safari terminates WebSocket connections immediately. Handling reconnection, heartbeat pings, and missed messages adds hundreds of lines of client code.

Third, infrastructure cost. Hosting a WebSocket server requires persistent server memory to hold open idle connections, or paying fifty dollars a month for a hosted pub/sub service.

For simple job completion notifications, WebSockets are over-engineered.

The problem with HTTP Polling

The lazy alternative to WebSockets is client-side polling.

A React component sets an interval with setInterval() and fetches /api/jobs/status?id=123 every two seconds until the status changes from pending to completed.

On your local machine, this looks harmless. In production, polling creates significant friction:

First, it wastes battery and mobile bandwidth by sending dozens of empty HTTP requests back and forth.

Second, on serverless hosting like Vercel, every single poll is a billable serverless function invocation. A task that takes one minute to complete triggers thirty function invocations for a single user, quickly blowing through your monthly invocation limits.

Third, it adds unnecessary database load, querying the database table repeatedly for rows that have not changed.

There is a superior standard that web developers frequently overlook: Server-Sent Events.

Why Server-Sent Events (SSE) are the sweet spot

Server-Sent Events (SSE) are a native web standard built directly into every modern web browser via the EventSource API.

Unlike WebSockets, SSE operates over standard HTTP. It provides a persistent, one-way stream from the server down to the client.

Here is why SSE fits Next.js background jobs perfectly:

  1. Standard HTTP/2 multiplexing: SSE connections ride on the existing HTTP/2 connection. They do not consume extra TCP sockets or require special firewall rules.
  2. Built-in reconnection: If the user loses WiFi or locks their phone, the browser's native EventSource handles reconnection automatically when connectivity returns.
  3. Zero third-party bills: SSE runs directly through your Next.js application server and Nginx reverse proxy. You do not need Pusher, Ably, or Socket.io.
  4. Automatic React Server Component integration: When an SSE event fires, you can simply call router.refresh() to fetch the updated server data cleanly.

How BoringPush implements real-time updates

In BoringPush, real-time updates are handled through a clean, eighty-line utility located in libs/live.js.

The implementation uses HMAC signatures to ensure security, meaning users can only listen to topics they are authorized to access.

Step 1: Add the <Live /> component to your page

In any Next.js page or layout where you want automatic updates, include the <Live /> component:

// app/(pages)/dashboard/orders/page.js
import Live from "@/components/Live";
import { getOrders } from "@/providers/db";

export default async function OrdersPage() {
  const orders = await getOrders();

  return (
    <div>
      {/* Listens for background updates */}
      <Live topic="orders" />
      <OrderList orders={orders} />
    </div>
  );
}

Behind the scenes, the server generates an HMAC-signed event URL. The client connects to /api/events and waits passively without consuming compute.

Step 2: Broadcast from your background worker

When your background worker script completes its task, it broadcasts an event with one line of code:

// Inside scripts/jobs/billing/process-order.mjs
import { broadcast } from "../../../libs/live.js";

export default async function (payload) {
  // 1. Process the payment and order
  await executeOrderProcessing(payload);

  // 2. Broadcast the update to the UI
  await broadcast("orders", "update");
}

The instant broadcast() is called on the server, the event pushes through the open SSE stream.

The browser receives the event and executes router.refresh(), updating the React Server Component tree with fresh data in fractions of a second.

The user sees their new order appear on screen without ever touching the reload button.

The architectural takeaway

You do not need to install complex WebSocket servers for standard application notifications. You do not need to pay a third-party vendor to tell a browser when a database record changed.

Use Server-Sent Events for one-way updates from your workers to your UI.

It uses native browser standards, works through standard proxies, and eliminates polling invoices forever.

FAQ

Does SSE keep working if the user locks their phone?

When a phone goes to sleep, the browser pauses the connection. As soon as the user unlocks their phone and returns to the tab, the native EventSource standard reconnects automatically and triggers an immediate refresh if updates occurred.

Can unauthorized users listen to private events?

No. In BoringPush, all topic URLs are signed server-side using SHA-256 HMAC tokens. The /api/events endpoint rejects any connection attempt where the topic signature does not match the authenticated user's session.

Does Nginx require special configuration for Server-Sent Events?

Yes. Nginx needs two lines to prevent proxy buffering from holding back event chunks: proxy_buffering off; and proxy_cache off;. The Nginx configuration templates included in BoringPush include these headers by default.

What is the maximum number of concurrent SSE connections?

Under HTTP/2, multiple SSE streams and standard web requests share a single underlying TCP connection to the domain. A $5 VPS can easily maintain thousands of idle concurrent SSE connections with negligible RAM usage.

How does this compare to React Server Actions?

Server Actions are client-initiated RPC calls. They cannot push updates to the browser when an asynchronous background task completes minutes later. SSE bridges that gap by allowing your detached worker to notify the browser when work is done.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading