Real-Time UI Updates with SSE: Simpler Than WebSockets

Muhib

Real-Time UI Updates with SSE: Simpler Than WebSockets

Ever built a web app where users click a button…and wait…and wait…while the server finishes some long-running job? Spinners are fine—but they leave users in the dark. Wouldn’t it be amazing if your UI could show live progress updates as things happen on the server, without the complexity of WebSockets? That’s exactly what Server-Sent Events (SSE) make possible.

In this post, I’ll walk you through:

Let’s go!

What is Server-Sent Events (SSE)?

SSE is a web standard that allows your server to push updates to the browser over a single HTTP connection. No WebSocket handshake. No polling. No fuss.

A Real-World Example: Streaming a Workflow Execution

Let’s imagine a real-life scenario: You’ve built an automation tool where users design workflows as graphs. Each node in the graph might:

  • Make API calls
  • Run computations
  • Chain into other nodes

Some nodes can take several seconds to execute.

Let’s say you have a UI where users can build a workflow like this:

Orchestrator
      ↓
ComputeAgent
      ↓
StorageAgent

The Native Approach

A native implementation might:

Frontend → POST /api/run-workflow
    → run the entire workflow
    → backend does everything
    → Respond to the browser with the **final result only**

Frontend displays final result.

The problem:

When the user clicks “Run,” you don’t want them staring at a blank spinner for 30 seconds. Instead you want something like:

That’s where SSE saves the day.


How SSE Solves This

Instead of responding once at the end, the server streams progress messages as they happen.

Your browser listens for events and updates the UI instantly. Instead of one big JSON response, you stream chunks like this:

Example messages:

data: { "event": "node-started", "nodeId": "abc" }

data: { "event": "node-completed", "nodeId": "abc", "result": "..." }

data: { "event": "workflow-complete" }

That’s powerful because:

How Does SSE Work?


Why Not WebSockets?

You might ask:

Why not just use WebSockets?

Great question!

WebSockets are fantastic—but they’re:

SSE is perfect when:


Practical Implementation in Next.js

Let’s dive into a working example.

I’m using:

The goal:

Run a workflow, node by node, and stream progress messages in real-time to the browser.

Backend SSE Route

Here’s a complete Next.js route:

export async function GET(req, { params }) {
  const { id } = params;

  // Load workflow from DB
  const workflow = await getWorkflowById(id);

  if (!workflow) {
    return new Response(JSON.stringify({ error: "Workflow not found" }), {
      status: 404,
    });
  }

  let flow;
  try {
    flow = JSON.parse(workflow.data.flow);
  } catch (e) {
    return new Response(JSON.stringify({ error: "Invalid workflow data" }), {
      status: 500,
    });
  }

  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const send = (data) => {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify(data)}\n\n`),
          );
        };

        const context = {};
        const visited = new Set();

        const { nodes, edges } = flow;
        const targets = new Set(edges.map((e) => e.target));
        const startNodes = nodes.filter((n) => !targets.has(n.id));

        for (const start of startNodes) {
          await runNodeRecursive(
            start.id,
            nodes,
            edges,
            visited,
            send,
            context,
          );
        }

        send({ event: "workflow-complete" });
        controller.enqueue(encoder.encode("\n"));
        controller.close();
      } catch (err) {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ error: err.message })}\n\n`),
        );
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

How It Works

runNodeRecursive(...)

{ event: "node-started", nodeId: "123" }
{ event: "node-completed", nodeId: "123", result: "..." }

Frontend: React Chatbot UI

On the client side, it’s beautifully simple.

Example:

const handleRunWorkflow = (userPrompt) => {
  setMessages((prev) => [...prev, { type: "user", text: userPrompt }]);
  setRunning(true);

  const eventSource = new EventSource(
    `/api/workflows/${workflowId}/run?prompt=${encodeURIComponent(userPrompt)}`,
  );

  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);

    if (data.event === "node-started") {
      setMessages((prev) => [
        ...prev,
        {
          type: "system",
          text: `Node ${data.nodeId} (${data.type}) started`,
        },
      ]);
    } else if (data.event === "node-completed") {
      setMessages((prev) => [
        ...prev,
        {
          type: "bot",
          text: `Node ${data.nodeId} result:\n${data.result}`,
        },
      ]);
    } else if (data.event === "workflow-complete") {
      setMessages((prev) => [
        ...prev,
        {
          type: "system",
          text: "Workflow complete!",
        },
      ]);
      setRunning(false);
      eventSource.close();
    }
  };

  eventSource.onerror = (err) => {
    console.error("SSE error", err);
    setMessages((prev) => [
      ...prev,
      { type: "error", text: "SSE connection error" },
    ]);
    setRunning(false);
    eventSource.close();
  };
};

What Happens

When the user submits a prompt:

data: { "event": "node-started", ... }
Node Orchestrator started
Node Orchestrator result: ...
...
Workflow complete!

The Benefits

SSE makes your app:


Downsides of SSE

SSE is brilliant, but:

But for modern apps—it’s usually perfect.

The Result

This approach turned my boring “wait and spin” workflows into a real-time conversation. Users can see:

All thanks to a few elegant lines of streaming code.


Conclusion

If you’re running:

…and you want users to see real progress, SSE might be the perfect solution.

Have you tried SSE in production?

Drop a comment and share your war stories or reach out on [email protected] — I’d love to help or hear your thoughts.
Social Links:
Youtube | GitHub | LinkedIn

Thank you, Muhib.

Enjoy this post?
Buy me a Coffee