<- Back to Software Development

Short Polling vs Long Polling: Choosing a Status Update Strategy

June 4, 20269 min read
Share

When a client needs changing server state, such as delivery location, payment confirmation, file-processing progress, or notification updates, it must repeatedly learn whether something new has happened. Short polling and long polling are two HTTP-based strategies for solving this problem.

The main difference is not that one uses HTTP and the other does not. Both use normal HTTP requests. The difference is when the client sends requests and when the server decides to respond.

Short Answer

Short polling means the client asks the server for the current status at a fixed interval.

Long polling means the client sends a request, and the server holds that request until the status changes or a timeout is reached. Once the response returns, the client immediately sends another waiting request if more updates are still needed.

StrategyClient BehaviourServer BehaviourSuitable Situation
Short pollingSends a request every few secondsResponds immediatelySimple status checking with acceptable delay
Long pollingKeeps one waiting request activeResponds when data changes or timeout occursIrregular updates that should appear quickly
Server-Sent EventsOpens a one-way event streamPushes multiple updates continuouslyFrequent server-to-browser updates
WebSocketsKeeps a two-way connectionSends and receives messages continuouslyChat, games, live collaboration

Short polling is easier to implement. Long polling can reduce repeated empty responses and deliver updates faster, but the server must manage waiting connections correctly.

The Core Difference

Assume a delivery order remains unchanged for two minutes and then changes from preparing to delivered.

With short polling every five seconds, the browser may make about 24 requests before receiving the useful result:

Client                         Server
  | --- status? -------------> |
  | <--- preparing ----------- |
  |                            |
  | --- status? -------------> |
  | <--- preparing ----------- |
  |                            |
  | --- status? -------------> |
  | <--- preparing ----------- |
  |                            |
  |        repeated many times |
  |                            |
  | --- status? -------------> |
  | <--- delivered ----------- |

With long polling, the browser makes a request and waits. The server returns only when the status changes or when the request timeout expires:

Client                         Server
  | --- wait for update -----> |
  |                            |  request remains open
  |                            |  status changes
  | <--- delivered ----------- |
  | --- wait for update -----> |  only if more updates are needed

Long polling is not one permanent request. It is a sequence of longer HTTP requests. Each response ends the current request, and the client creates a new one when it still needs future updates.

How Short Polling Works

In short polling, the frontend schedules repeated API calls. The server does not wait for a change; it simply returns the current value whenever it is asked.

A client should avoid starting a new request while the previous request is still running. This example waits for each response before scheduling the next check:

type JobStatus = "processing" | "completed" | "failed";

async function pollJobStatus(jobId: string): Promise<void> {
  const response = await fetch(`/api/jobs/${jobId}/status`);
  const result: { status: JobStatus } = await response.json();

  console.log(result.status);

  if (result.status === "completed" || result.status === "failed") {
    return;
  }

  setTimeout(() => {
    void pollJobStatus(jobId);
  }, 5000);
}

void pollJobStatus("job_123");

The backend endpoint is simple because it responds immediately:

import { Request, Response } from "express";

const jobStatus = new Map<string, string>([
  ["job_123", "processing"]
]);

export function getJobStatus(req: Request, res: Response): void {
  const status = jobStatus.get(req.params.jobId);

  if (!status) {
    res.status(404).json({ error: "Job not found" });
    return;
  }

  res.json({ status });
}

This approach is practical when a user checks one short-running job or when updates do not need to appear instantly.

The inefficiency appears when many clients ask frequently but the state rarely changes. For example, 10,000 clients polling every five seconds create around 2,000 requests per second, even if no job status has changed.

How Long Polling Works

In long polling, the client sends the request immediately after the previous request completes. It does not sleep for a fixed interval because the waiting period happens inside the request itself.

type JobStatus = "processing" | "completed" | "failed";

async function waitForJobStatus(jobId: string): Promise<void> {
  while (true) {
    const response = await fetch(`/api/jobs/${jobId}/status/wait`);
    const result: {
      status: JobStatus;
      timedOut: boolean;
    } = await response.json();

    console.log(result.status);

    if (result.status === "completed" || result.status === "failed") {
      return;
    }
  }
}

void waitForJobStatus("job_123");

The backend first checks the current status. If the job is already finished, it answers immediately. Otherwise, it waits for either an update event or a timeout:

import { EventEmitter } from "node:events";
import { Request, Response } from "express";

const jobEvents = new EventEmitter();
const jobStatus = new Map<string, string>([
  ["job_123", "processing"]
]);

export function waitForJobStatus(req: Request, res: Response): void {
  const jobId = req.params.jobId;
  const currentStatus = jobStatus.get(jobId);

  if (!currentStatus) {
    res.status(404).json({ error: "Job not found" });
    return;
  }

  if (currentStatus !== "processing") {
    res.json({ status: currentStatus, timedOut: false });
    return;
  }

  const eventName = `job:${jobId}:updated`;

  const onUpdated = (status: string): void => {
    cleanup();
    res.json({ status, timedOut: false });
  };

  const timeoutId = setTimeout(() => {
    cleanup();
    res.json({ status: "processing", timedOut: true });
  }, 30000);

  const cleanup = (): void => {
    clearTimeout(timeoutId);
    jobEvents.off(eventName, onUpdated);
  };

  jobEvents.once(eventName, onUpdated);

  req.on("close", () => {
    cleanup();
  });
}

When the worker completes the job, it updates the stored state and publishes an event so waiting requests can respond:

export function completeJob(jobId: string): void {
  jobStatus.set(jobId, "completed");
  jobEvents.emit(`job:${jobId}:updated`, "completed");
}

In Node.js, this waiting does not mean the JavaScript process is continuously executing work for each request. The event loop can wait for an event or timeout without blocking one thread per client. However, the application still holds sockets, response objects, timeout handlers, and memory while those requests remain open.

Client and Server Responsibilities

The frontend changes in long polling as well as the backend. It is incorrect to think that only the server implementation is different.

ResponsibilityShort PollingLong Polling
When to send the next requestAfter a fixed delayImmediately after the previous response
What happens when nothing changesServer returns unchanged statusServer keeps waiting until timeout
Request overlap riskPossible if using careless intervalsNormally one active request per resource
Timeout handlingOptional but still usefulRequired for stable reconnect behaviour
Server resource managementShort-lived requestsOpen connections and cleanup required

For short polling, a common mistake is using setInterval() without considering request duration. If an API call takes longer than the interval, multiple status requests may overlap.

For long polling, the important rule is that one browser tab should normally keep only one pending request for a watched resource. Once it receives a response, it decides whether to stop or open the next request.

Production Trade-Offs

Short Polling: Minimal Complexity

The API returns immediately and is easy to operate. It is often the correct starting point for small features or low traffic.

Short Polling: Repeated Waste

When data changes rarely, most requests return exactly the same status while still consuming network and backend capacity.

Long Polling: Better Responsiveness

A client can receive an update shortly after it happens instead of waiting for the next polling interval.

Long Polling: More Infrastructure Work

The server must handle open requests, timeouts, disconnected clients, scaling across instances, and connection limits.

A local long-polling prototype may use an in-memory EventEmitter, but this does not work reliably after the application is deployed across several backend instances.

Consider this deployment:

Client waiting request  ---> Server A
Background job update   ---> Server B

If Server B only emits an in-memory event, Server A never receives it. The client's waiting request remains open until timeout even though the job already completed.

In a multi-instance deployment, the update should normally be distributed using shared infrastructure such as Redis Pub/Sub, a message broker, or a durable job-status store combined with notification events.

Production ConcernProblemPractical Response
Reverse proxy timeoutProxy closes a long request before the application respondsUse an application timeout shorter than the proxy limit
Client closes the tabServer may continue tracking a useless waitRemove listeners on connection close
Multiple server instancesUpdate and waiting request reach different processesUse shared event delivery
Too many waiting clientsSocket and memory consumption risesMeasure active requests and enforce limits
AuthorizationA user may query another user's jobAuthorize every polling request
Retry stormsMany clients reconnect at the same momentAdd bounded retry delay or jitter

When to Use Each Strategy

Use short polling when:

  • The page has low traffic.
  • The state only needs to refresh every several seconds.
  • The operation is temporary, such as waiting for a single file upload or payment status.
  • You want the simplest reliable implementation first.

Use long polling when:

  • Status changes are unpredictable.
  • The UI should update quickly after the server state changes.
  • Frequent unchanged short-polling responses have become measurable waste.
  • A normal HTTP request model is still preferred over a streaming or socket-based connection.

Use Server-Sent Events when the server continuously sends one-way updates to the browser, such as live progress, notifications, or monitoring data.

Use WebSockets when both the client and server need frequent low-latency messages, such as chat, multiplayer activity, or collaborative editing.

RequirementReasonable Starting Choice
Refresh an admin dashboard every 30 secondsShort polling
Check whether a generated report has completedShort polling first; long polling if needed
Display delivery-status changes with low delayLong polling or Server-Sent Events
Stream recurring server updatesServer-Sent Events
Support two-way real-time interactionWebSockets

The correct choice is not determined by which technique is more advanced. It is determined by update frequency, acceptable delay, expected traffic, and operational complexity.

Common Mistakes

Assuming Long Polling Creates One Permanent Connection

Long polling still uses repeated HTTP request-response cycles. Each request waits longer, but it eventually returns or times out.

Holding a Database Connection While Waiting

A waiting HTTP response should not keep a database transaction or dedicated database connection open for thirty seconds. Read current state, wait on an application-level event, then read again only when necessary.

Forgetting Timeout and Disconnect Cleanup

Without timeout and disconnect handling, abandoned waiting requests can remain registered in server memory and increase resource usage.

Using In-Memory Events in a Scaled Backend

An in-memory listener is sufficient only when the relevant request and update occur in the same Node.js process. Horizontal scaling usually requires a shared event channel.

A Practical Implementation Order

  1. Start with short polling and a reasonable interval, such as five or ten seconds.
  2. Measure request count, response duplication, user-visible update delay, and backend cost.
  3. Change to long polling only when request waste or response delay becomes meaningful.
  4. Add timeout handling, disconnect cleanup, authorization, and monitoring.
  5. Introduce a shared event mechanism when deploying multiple backend instances.
  6. Choose Server-Sent Events or WebSockets only when the communication pattern actually requires persistent streaming or two-way interaction.

The Main Principle

Short polling repeatedly asks, “Has anything changed yet?” Long polling asks once and lets the server wait before answering.

Start with the simpler approach that satisfies the user experience. Move to long polling when measurement shows that repeated unchanged requests create unnecessary work or that waiting for the next interval creates unacceptable delay.

当客户端需要持续获得服务器上的最新状态时,例如外卖配送位置、付款是否成功、文件处理进度,或者新的通知,它就需要一种机制来反复确认:服务器的数据有没有发生变化。

短轮询(Short Polling)与长轮询(Long Polling)都是基于普通 HTTP 请求的方案。它们真正的差别不在于 API 长什么样,而在于:客户端什么时候发下一次请求,以及服务器什么时候返回响应。

简短答案

短轮询是客户端每隔固定时间请求一次服务器,无论数据有没有变化,服务器都会马上返回当前状态。

长轮询是客户端发送请求后,服务器先不立即返回,而是等到数据真的改变,或者等待超时后才响应。客户端收到响应后,如果还需要继续监听,就立刻发送下一次长轮询请求。

策略客户端行为服务器行为适合场景
短轮询每隔几秒请求一次马上返回当前状态简单状态查询,可接受一定延迟
长轮询保持一个等待中的请求状态改变或超时后返回更新不固定,但希望尽快显示
Server-Sent Events打开单向事件流连续推送更新服务器频繁推送给浏览器
WebSockets保持双向连接双方都可持续发消息聊天、实时协作、多人游戏

短轮询更容易实现。长轮询可以减少大量“状态完全没变”的重复响应,并让更新更快出现在前端,但服务器要承担更多连接管理责任。

核心差异

假设一个配送订单在两分钟内一直维持 preparing 状态,最后才变成 delivered

如果前端每五秒进行一次短轮询,那么在真正拿到有意义的更新前,浏览器可能已经请求了大约 24 次:

客户端                         服务器
  | --- 查询状态?-----------> |
  | <--- preparing ---------- |
  |                            |
  | --- 查询状态?-----------> |
  | <--- preparing ---------- |
  |                            |
  | --- 查询状态?-----------> |
  | <--- preparing ---------- |
  |                            |
  |        重复很多次          |
  |                            |
  | --- 查询状态?-----------> |
  | <--- delivered ---------- |

如果使用长轮询,浏览器先发出一次请求,然后等待服务器返回。当订单状态真的改变时,服务器才立即把新状态返回:

客户端                         服务器
  | --- 等待状态更新 --------> |
  |                            |  请求保持等待
  |                            |  状态发生变化
  | <--- delivered ---------- |
  | --- 等待下一次更新 ------> |  仅在仍需要监听时继续

长轮询并不是只发送一次永远不结束的请求。它仍然是一个接一个的 HTTP 请求,只是每次请求可以等待比较久,而且更有机会返回真正改变过的数据。

短轮询如何实现

在短轮询中,前端负责按照间隔反复调用 API。服务器不需要等待事件发生;每一次被询问时,它只要读取当前状态并直接返回。

前端实现时,不应该在上一次请求还没有结束前,又机械式地发送下一次请求。下面的写法会等到响应完成后,才安排下一次查询:

type JobStatus = "processing" | "completed" | "failed";

async function pollJobStatus(jobId: string): Promise<void> {
  const response = await fetch(`/api/jobs/${jobId}/status`);
  const result: { status: JobStatus } = await response.json();

  console.log(result.status);

  if (result.status === "completed" || result.status === "failed") {
    return;
  }

  setTimeout(() => {
    void pollJobStatus(jobId);
  }, 5000);
}

void pollJobStatus("job_123");

后端接口很简单,因为它不需要把请求挂住等待:

import { Request, Response } from "express";

const jobStatus = new Map<string, string>([
  ["job_123", "processing"]
]);

export function getJobStatus(req: Request, res: Response): void {
  const status = jobStatus.get(req.params.jobId);

  if (!status) {
    res.status(404).json({ error: "Job not found" });
    return;
  }

  res.json({ status });
}

这种方式适合流量不高、状态更新不要求即时出现的场景。例如用户提交一份报表后,页面每五秒确认一次是否完成,通常已经足够。

问题在于,当大量用户都频繁查询,而数据其实很少变化时,系统会处理很多没有新信息的请求。例如,10,000 个客户端每五秒请求一次,即使所有任务状态都没有改变,服务器仍然会接收到大约每秒 2,000 次查询。

长轮询如何实现

长轮询中,客户端不再固定等待五秒后才询问服务器。它发送请求后,就让这个请求在服务器端等待;一旦收到响应,若仍需要后续更新,便立刻打开下一次等待请求。

type JobStatus = "processing" | "completed" | "failed";

async function waitForJobStatus(jobId: string): Promise<void> {
  while (true) {
    const response = await fetch(`/api/jobs/${jobId}/status/wait`);
    const result: {
      status: JobStatus;
      timedOut: boolean;
    } = await response.json();

    console.log(result.status);

    if (result.status === "completed" || result.status === "failed") {
      return;
    }
  }
}

void waitForJobStatus("job_123");

后端收到请求时,先检查任务是否已经结束。如果任务已经完成,服务器可以立即返回;如果仍在处理中,服务器就等待状态更新事件,或者等待超时后返回:

import { EventEmitter } from "node:events";
import { Request, Response } from "express";

const jobEvents = new EventEmitter();
const jobStatus = new Map<string, string>([
  ["job_123", "processing"]
]);

export function waitForJobStatus(req: Request, res: Response): void {
  const jobId = req.params.jobId;
  const currentStatus = jobStatus.get(jobId);

  if (!currentStatus) {
    res.status(404).json({ error: "Job not found" });
    return;
  }

  if (currentStatus !== "processing") {
    res.json({ status: currentStatus, timedOut: false });
    return;
  }

  const eventName = `job:${jobId}:updated`;

  const onUpdated = (status: string): void => {
    cleanup();
    res.json({ status, timedOut: false });
  };

  const timeoutId = setTimeout(() => {
    cleanup();
    res.json({ status: "processing", timedOut: true });
  }, 30000);

  const cleanup = (): void => {
    clearTimeout(timeoutId);
    jobEvents.off(eventName, onUpdated);
  };

  jobEvents.once(eventName, onUpdated);

  req.on("close", () => {
    cleanup();
  });
}

当后台任务完成时,它更新任务状态,并发出事件通知所有正在等待的请求:

export function completeJob(jobId: string): void {
  jobStatus.set(jobId, "completed");
  jobEvents.emit(`job:${jobId}:updated`, "completed");
}

在 Node.js 中,请求处于等待状态,不代表程序必须为每一个等待中的客户端持续执行一段循环。Node.js 可以通过事件循环等待事件或 timeout,而不是为每一个连接阻塞一条线程。

但是,长轮询并不是没有成本。只要请求还没有结束,服务器仍然需要保留 socket、response 对象、timeout 处理器以及相关内存。

前端与后端分别改变了什么

长轮询并不是只改后端,前端请求逻辑也会发生变化。

责任短轮询长轮询
下一次请求何时发送等固定间隔后发送上一次响应结束后立即发送
没有新数据时怎么办服务器仍返回相同状态服务器继续等待,直到超时
是否容易产生重叠请求使用错误的定时器写法时容易发生通常维持一个等待中的请求
timeout 是否重要有用,但不是核心机制必须处理
后端主要负担大量短请求持续中的连接与清理工作

短轮询中,一个常见错误是直接使用 setInterval(),而没有考虑 API 响应可能比间隔更慢。如果五秒发送一次请求,但某次服务器八秒才返回,那么页面上可能同时存在多个重复的状态查询。

长轮询中,正确思路通常是:一个页面对于一个资源,只维护一个正在等待的请求。收到回应后,再决定终止监听还是继续发送下一次等待请求。

生产环境的取舍

短轮询:实现简单

API 每次都立即返回,前后端逻辑容易理解,也比较容易部署与排查。小型功能通常应该先从这里开始。

短轮询:容易产生浪费

当状态很少改变时,大量请求只是反复获得同一个结果,却仍然消耗网络与服务器资源。

长轮询:更新更及时

状态改变后,服务器可以很快返回结果,不需要让用户等到下一次固定查询时间。

长轮询:运维复杂度更高

后端必须处理等待中的连接、超时、断线清理、多实例部署,以及连接数量限制。

在本机开发时,你可能会用 Node.js 内存中的 EventEmitter 完成长轮询。但是当系统部署成多个后端实例时,这种实现就不可靠了。

例如:

客户端等待请求  ---> Server A
后台任务更新    ---> Server B

如果 Server B 只是发出自己进程内的事件,Server A 根本不会知道状态已经改变。结果就是:任务实际上完成了,但客户端等待的请求仍然只能等到 timeout 才返回。

因此,在多实例部署中,状态更新通常需要通过共享机制通知其他服务器,例如 Redis Pub/Sub、消息队列,或者一个共享状态储存层配合事件通知。

生产问题可能造成的影响实际处理方式
反向代理 timeout长请求被代理层提前关闭应用 timeout 设得比代理限制更短
用户关闭页面服务器继续追踪无效等待请求监听连接关闭并清理 listener
多个后端实例更新与等待请求落在不同进程使用共享事件机制
等待连接太多socket 与内存使用量上升监控活动连接并设置限制
缺少权限检查用户可能读取别人的任务状态每次请求都验证权限
大量客户端同时重连瞬间产生请求高峰增加有限延迟或随机抖动

什么时候选择哪一种

适合使用短轮询的情况:

  • 页面流量不高。
  • 状态晚几秒出现也不会影响用户体验。
  • 用户只是在短时间内等待一个任务完成,例如付款确认或档案生成。
  • 当前阶段更重视简单、稳定、容易维护的实现。

适合使用长轮询的情况:

  • 状态改变的时间无法预测。
  • 状态一旦改变,界面应该尽快更新。
  • 大量短轮询请求都返回未变化的数据,已经形成明显浪费。
  • 系统仍希望使用一般 HTTP 请求,而暂时不引入事件流或 socket 连接。

适合使用 Server-Sent Events 的情况,是服务器需要持续、单向地向浏览器发送更新,例如通知列表、处理进度或监控数据。

适合使用 WebSockets 的情况,是客户端和服务器双方都需要频繁、低延迟地互相传递信息,例如聊天室、多人协作编辑或即时游戏。

需求合理的初始选择
管理后台每 30 秒刷新一次状态短轮询
等待一份生成中的报表完成先用短轮询,需要时改成长轮询
尽快显示配送状态变化长轮询或 Server-Sent Events
服务器持续推送更新Server-Sent Events
双向实时互动WebSockets

不要因为长轮询听起来更进阶,就直接使用长轮询。正确选择取决于数据变化频率、用户可以接受的延迟、预计流量,以及你是否愿意承担额外的部署复杂度。

常见误区

误区一:以为长轮询只发送一次请求

长轮询依然会重复发送 HTTP 请求。只是每次请求会等待更久,而且更可能在返回时带有真正的新数据。

误区二:等待期间持续占用数据库连接

一个等待中的 HTTP 请求,不应该长时间持有数据库 transaction 或专用数据库连接。正确方式通常是先读取当前状态,再等待应用事件,必要时才重新查询数据库。

误区三:没有处理超时与断线

用户关闭页面、网络中断,或代理层主动断开连接时,服务器必须清除对应的监听器与等待数据,否则无效请求会慢慢累积。

误区四:部署多台服务器后仍只使用内存事件

内存中的事件只能通知同一个 Node.js 进程。只要系统开始水平扩展,就通常需要一个共享的事件传递机制。

实作顺序

  1. 先使用短轮询,并选择合理间隔,例如五秒或十秒。
  2. 记录请求数量、重复响应比例、用户感受到的更新时间,以及后端处理成本。
  3. 只有当重复请求造成明显浪费,或更新延迟已经影响体验时,才改成长轮询。
  4. 为长轮询加入 timeout、断线清理、权限验证与监控。
  5. 当后端部署成多个实例时,引入共享事件传递机制。
  6. 只有在确实需要持续推送或双向实时沟通时,才进一步采用 Server-Sent Events 或 WebSockets。

核心原则

短轮询的思路是不断询问服务器:“现在有变化了吗?”长轮询的思路是发送一次请求,然后让服务器在有变化时再回答。

实现系统时,应先选择能够满足体验需求的最简单方案。只有当实际测量显示大量请求都没有带回新数据,或者固定等待间隔造成明显延迟时,才值得把设计升级成长轮询或更实时的通信方式。

In this series

Traffic Control

View series ->

Part 4 of 4. Move between logs in the same learning sequence.