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.
| Strategy | Client Behaviour | Server Behaviour | Suitable Situation |
|---|---|---|---|
| Short polling | Sends a request every few seconds | Responds immediately | Simple status checking with acceptable delay |
| Long polling | Keeps one waiting request active | Responds when data changes or timeout occurs | Irregular updates that should appear quickly |
| Server-Sent Events | Opens a one-way event stream | Pushes multiple updates continuously | Frequent server-to-browser updates |
| WebSockets | Keeps a two-way connection | Sends and receives messages continuously | Chat, 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.
| Responsibility | Short Polling | Long Polling |
|---|---|---|
| When to send the next request | After a fixed delay | Immediately after the previous response |
| What happens when nothing changes | Server returns unchanged status | Server keeps waiting until timeout |
| Request overlap risk | Possible if using careless intervals | Normally one active request per resource |
| Timeout handling | Optional but still useful | Required for stable reconnect behaviour |
| Server resource management | Short-lived requests | Open 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 Concern | Problem | Practical Response |
|---|---|---|
| Reverse proxy timeout | Proxy closes a long request before the application responds | Use an application timeout shorter than the proxy limit |
| Client closes the tab | Server may continue tracking a useless wait | Remove listeners on connection close |
| Multiple server instances | Update and waiting request reach different processes | Use shared event delivery |
| Too many waiting clients | Socket and memory consumption rises | Measure active requests and enforce limits |
| Authorization | A user may query another user's job | Authorize every polling request |
| Retry storms | Many clients reconnect at the same moment | Add 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.
| Requirement | Reasonable Starting Choice |
|---|---|
| Refresh an admin dashboard every 30 seconds | Short polling |
| Check whether a generated report has completed | Short polling first; long polling if needed |
| Display delivery-status changes with low delay | Long polling or Server-Sent Events |
| Stream recurring server updates | Server-Sent Events |
| Support two-way real-time interaction | WebSockets |
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
- Start with short polling and a reasonable interval, such as five or ten seconds.
- Measure request count, response duplication, user-visible update delay, and backend cost.
- Change to long polling only when request waste or response delay becomes meaningful.
- Add timeout handling, disconnect cleanup, authorization, and monitoring.
- Introduce a shared event mechanism when deploying multiple backend instances.
- 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 进程。只要系统开始水平扩展,就通常需要一个共享的事件传递机制。
实作顺序
- 先使用短轮询,并选择合理间隔,例如五秒或十秒。
- 记录请求数量、重复响应比例、用户感受到的更新时间,以及后端处理成本。
- 只有当重复请求造成明显浪费,或更新延迟已经影响体验时,才改成长轮询。
- 为长轮询加入 timeout、断线清理、权限验证与监控。
- 当后端部署成多个实例时,引入共享事件传递机制。
- 只有在确实需要持续推送或双向实时沟通时,才进一步采用 Server-Sent Events 或 WebSockets。
核心原则
短轮询的思路是不断询问服务器:“现在有变化了吗?”长轮询的思路是发送一次请求,然后让服务器在有变化时再回答。
实现系统时,应先选择能够满足体验需求的最简单方案。只有当实际测量显示大量请求都没有带回新数据,或者固定等待间隔造成明显延迟时,才值得把设计升级成长轮询或更实时的通信方式。