{} The Go Reference

Apis · Web · Beginner

Polling

When the client just asks again — short polling on a timer vs long polling that holds the request open, their costs, and when polling is the right boring choice.

Apis Beginner ⏱ 4 min read Complete

🚗 Analogy

Polling is the kid in the back seat: “Are we there yet? … Are we there yet?” The client just keeps asking. Short polling asks on a timer; long polling asks once and waits quietly until the driver actually has an answer, then immediately asks again. It’s the least clever approach — which is exactly why it’s so reliable.

Short polling: ask on a timer

The client requests the same endpoint every few seconds; the server answers with the current state. Dead simple, works through any proxy — here’s the loop, driven in-process:

graph LR
C["client"] -->|"GET /status (t=0s)"| S["server"]
S -->|"version=1"| C
C -->|"GET /status (t=5s)"| S
S -->|"version=2"| C
C -->|"GET /status (t=10s)"| S
S -->|"version=3"| C
polling.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
)

func main() {
// server state that changes between requests
version := 0
status := func(w http.ResponseWriter, r *http.Request) {
	version++ // pretend new data arrived since the last poll
	fmt.Fprintf(w, "version=%d", version)
}

// the client polls the same endpoint on a timer (here: 3 times)
for poll := 1; poll <= 3; poll++ {
	rec := httptest.NewRecorder()
	status(rec, httptest.NewRequest("GET", "/status", nil))
	fmt.Printf("poll %d -> %s\n", poll, rec.Body.String())
}
}

In real code the loop is a time.Ticker; a polite client adds jitter and backoff so a thousand clients don’t all hit the server on the same second (the thundering herd).

Long polling: hold the request open

Instead of answering immediately, the server parks the request until there’s something new (or a timeout fires), then responds; the client re-requests at once. Fewer empty round-trips, near-real-time — at the cost of a held connection per waiting client:

func longPoll(w http.ResponseWriter, r *http.Request) {
	select {
	case ev := <-updates:           // data arrived — answer now
		fmt.Fprint(w, ev)
	case <-time.After(30 * time.Second): // nothing yet — let the client retry
		w.WriteHeader(http.StatusNoContent)
	case <-r.Context().Done():      // client gave up
		return
	}
}

Choosing

graph TD
Q["need updates?"] --> A["rarely / non-urgent<br/>→ short polling"]
Q --> B["near real-time, server → client<br/>→ long polling or SSE"]
Q --> C["two-way, high frequency<br/>→ WebSockets"]

Reach up the ladder only when you need to: short pollinglong pollingSSEWebSockets. Each step adds real-timeness and complexity.

🐹 Boring is a feature

Polling has no special server, no held connections (short polling), and no proxy surprises — it just works, everywhere. Add backoff with jitter to avoid synchronized stampedes, send a small ETag/If-None-Match or a since cursor so unchanged polls return a cheap 304 Not Modified, and use a context timeout on the client. Don’t reach for WebSockets to update a number every minute.

See also

Next: the inverse arrangement — let the other service knock on your door with Webhooks.

Check your understanding

Score: 0 / 5

1. What is the difference between short polling and long polling?

Short polling asks every N seconds whether you like it or not — simple but wasteful. Long polling parks the request server-side until there's something to send (or a timeout), giving near-real-time updates with far fewer empty responses.

2. What's the main downside of frequent short polling?

If data changes rarely, most polls return 'no change', so you pay request overhead for nothing — and you still lag up to one interval behind. Long polling, SSE or WebSockets fix that; short polling wins on sheer simplicity.

3. When is plain polling actually the right choice?

Polling needs no special server infrastructure and sails through any HTTP proxy or firewall. For a status that updates every few minutes, a 30-second poll is simpler and more robust than holding open connections.

4. What does long polling trade away to get near-real-time updates?

Long polling parks each request server-side until data is ready, so every waiting client ties up a connection and a goroutine. That's cheap in Go (goroutines are light) but still bounded — past SSE/WebSocket scale you must cap concurrent waiters and set a timeout so parked requests eventually return.

5. How can a polling client make 'nothing changed' responses cheap?

Conditional requests turn an unchanged poll into a tiny 304 Not Modified (no body), saving bandwidth and serialization. The client sends the last ETag in If-None-Match (or a ?since= cursor); the server compares and either returns 304 or the new data with a fresh ETag.

Comments

Sign in with GitHub to join the discussion.