hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Real-Time APIs with @hectoday/http

Beyond Request-Response

  • Why Real-Time Matters
  • Project Setup

Polling

  • Short Polling
  • Long Polling

Server-Sent Events

  • How SSE Works
  • Building an SSE Endpoint
  • Event Types and IDs
  • SSE in Practice

WebSockets

  • How WebSockets Work
  • Building a WebSocket Server
  • Rooms and Broadcasting
  • Authentication on WebSockets
  • Handling Disconnects and Reconnection

Patterns and Architecture

  • Pub/Sub
  • Presence
  • Scaling Real-Time

Putting It All Together

  • Choosing the Right Approach
  • Capstone: Live Task Board

Short Polling

The simplest approach

Short polling is a loop: the client sends a request, processes the response, waits, and repeats. No special server support needed — any REST endpoint works.

The polling endpoint

Add a since parameter so the client only gets tasks updated after its last check:

route.get("/boards/:boardId/updates", {
  resolve: (c) => {
    const since = new URL(c.request.url).searchParams.get("since");

    let tasks;
    if (since) {
      tasks = db
        .prepare(
          "SELECT t.* FROM tasks t JOIN lists l ON t.list_id = l.id WHERE l.board_id = ? AND t.updated_at > ? ORDER BY t.updated_at",
        )
        .all(c.params.boardId, since);
    } else {
      tasks = db
        .prepare(
          "SELECT t.* FROM tasks t JOIN lists l ON t.list_id = l.id WHERE l.board_id = ? ORDER BY l.position, t.position",
        )
        .all(c.params.boardId);
    }

    return Response.json({
      data: tasks,
      timestamp: new Date().toISOString(),
    });
  },
});

The response includes a timestamp the client uses for the next poll.

The client

// Client-side
let lastTimestamp: string | null = null;

async function poll() {
  const url = lastTimestamp
    ? `/boards/board-1/updates?since=${encodeURIComponent(lastTimestamp)}`
    : `/boards/board-1/updates`;

  const res = await fetch(url);
  const { data, timestamp } = await res.json();
  lastTimestamp = timestamp;

  if (data.length > 0) {
    updateUI(data);
  }
}

// Poll every 5 seconds
setInterval(poll, 5000);
poll(); // Initial fetch

The waste problem

With 50 users polling every 5 seconds, the server handles 600 requests per minute. If nothing changed in the last hour, all 36,000 responses were empty.

# Simulate polling — most responses will be empty
for i in {1..10}; do
  curl -s "http://localhost:3000/boards/board-1/updates?since=2099-01-01T00:00:00Z" | jq '.data | length'
  sleep 1
done
# Output: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Ten requests. Zero useful results. This is the fundamental waste of polling.

When short polling is acceptable

Low-frequency updates. Checking for new email every 60 seconds. Refreshing a dashboard every 5 minutes. The update frequency matches the data change rate.

Few clients. An internal tool with 5 users polling every 10 seconds is 30 requests per minute. Negligible.

No persistent connection support. Some environments (serverless, restrictive proxies) cannot hold long-lived connections. Polling is the only option.

As a fallback. When SSE and WebSockets fail, polling always works.

Exercises

Exercise 1: Implement the polling endpoint. Start the server. Open two terminals: one polls, the other creates tasks. Verify the polling terminal picks up new tasks.

Exercise 2: Measure how many empty responses you get over 60 seconds of polling at 3-second intervals with no changes.

Exercise 3: Adjust the polling interval to 30 seconds. How does the experience change? (Answer: updates feel delayed. There is always a tradeoff between latency and resource usage with polling.)

What determines the right polling interval?

← Project Setup Long Polling →

© 2026 hectoday. All rights reserved.