What Is Long Polling?
Long polling is an HTTP communication technique in which a client sends a request and the server keeps it open until new data becomes available or a timeout occurs. Unlike traditional polling, where clients repeatedly request updates at fixed intervals, long polling allows the server to respond when something changes, reducing unnecessary requests and improving update latency.
Long polling is useful for notifications, job status updates, chat messages, and other applications that need near-real-time updates without maintaining a WebSocket connection. It works with ordinary HTTP infrastructure, but production implementations must handle connection lifetimes, client reconnections, missed events, and resource consumption carefully.
Table of Contents
- How Long Polling Works
- Long Polling vs. Other Communication Techniques
- Long Polling Request Lifecycle
- Implementing Long Polling with FastAPI
- Production Design Example
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Frequently Asked Questions
- Conclusion
How Long Polling Works
In ordinary HTTP polling, the client periodically asks whether new information is available. If the polling interval is five seconds, a change may remain undetected for nearly five seconds, while the server continues processing requests even when nothing changes.
Long polling changes when the server responds. Instead of immediately returning an empty result, the server waits for a relevant event, subject to a maximum timeout. Once the response is received, the client immediately starts another request to continue listening.
Basic Request Flow
A notification client using long polling follows this sequence:
- The client sends
GET /notifications?after=42. - The server checks whether notifications newer than ID 42 exist.
- If new notifications exist, the server responds immediately.
- Otherwise, the request remains pending until a notification arrives or the timeout expires.
- The client processes the response and updates its last-seen notification ID.
- The client immediately sends another request using the updated cursor.
The cursor is important because it separates event delivery from connection lifetime. If the connection fails, the client can reconnect and request events it has not yet processed.
A timeout does not necessarily indicate an error. A server may return HTTP 204 No Content after 25 seconds without new events, allowing the client to establish another request.
Why the Server Must Recheck State
A subtle race condition occurs when an application checks for new events and then registers a listener.
Suppose the server checks the database and finds no new notifications. A notification arrives immediately afterward, before the server starts waiting. If the implementation only waits for future notifications, it may miss the event and hold the request until timeout.
A safe implementation registers its notification mechanism before checking the authoritative event state, or rechecks that state after subscribing. This closes the gap between checking and waiting.
The notification signal should also be treated as a wake-up hint, not as the authoritative event record. A durable event store or database is needed when clients must recover events after disconnections.
Long Polling vs. Other Communication Techniques
Long polling is one of several ways to deliver updates over HTTP-based infrastructure. The best choice depends on communication direction, update frequency, connection requirements, and operational complexity.
Short Polling vs. Long Polling
Short polling sends requests at a fixed interval. Long polling keeps a request pending until something changes or the server reaches its timeout.
| Characteristic | Short Polling | Long Polling |
|---|---|---|
| Response behavior | Usually immediate | May wait for an event |
| Idle request frequency | Determined by polling interval | Approximately one request per timeout |
| Update latency | Up to the polling interval, plus processing delay | Usually close to event delivery latency |
| Open connections | Typically short-lived | Remain open while waiting |
| Implementation | Simple | Requires asynchronous waiting and reconnection handling |
Long polling usually reduces empty responses when updates are infrequent. However, it can increase the number of concurrent open connections, making connection limits and proxy timeouts important.
Long Polling vs. Server-Sent Events
Server-Sent Events (SSE) use a persistent HTTP response to stream multiple events from server to client. Long polling completes the HTTP response after delivering an event or timing out, requiring another request.
SSE is often a better fit for continuous one-way event streams. Long polling remains useful when streaming responses are poorly supported by existing infrastructure or when each request must have a bounded lifetime.
Both techniques primarily deliver server-to-client updates. Clients send commands through separate HTTP requests.
Long Polling vs. WebSockets
WebSockets establish a persistent, bidirectional communication channel. Either side can send messages without initiating a new HTTP request for every exchange.
For interactive multiplayer applications, collaborative editing, or high-frequency bidirectional messaging, WebSockets are often more efficient.
Long polling can be simpler when updates are occasional, the application already uses HTTP request-response infrastructure, or network environments make persistent upgraded connections difficult.
The broader trade-offs between request-response and asynchronous communication are discussed in Synchronous vs Asynchronous Communication.
Long Polling Request Lifecycle
Production long polling depends on coordination between client timeouts, server deadlines, reverse proxies, and application event storage. The server must never assume that a connection will remain available indefinitely.
Timeouts and Connection Management
A typical long-poll timeout is measured in tens of seconds rather than minutes or hours. For example, an application may wait up to 25 seconds before returning an empty response.
The server timeout should be shorter than the relevant proxy or load balancer idle timeout. Otherwise, infrastructure may close the connection before the application returns its intended response.
Long polling does not normally need application-level heartbeats within each response because the request is deliberately bounded. The next request acts as a continuation of the polling cycle.
HTTP/2 can multiplex multiple requests over one transport connection, but each pending request still consumes server-side state. HTTP/2 does not eliminate the need for connection and concurrency limits.
Handling Disconnections and Reconnection
Clients can disconnect because of mobile network changes, browser navigation, application shutdown, or proxy timeouts.
A disconnected request should release server resources promptly. An asynchronous server can monitor request cancellation or client disconnect signals and stop waiting when the client is gone.
On reconnection, the client should use its last successfully processed event cursor. Without a cursor or another replay mechanism, updates occurring between connections may be missed.
Repeated connection failures also require backoff. Immediately retrying thousands of disconnected clients can create a reconnect storm after a network outage.
Event Cursors and Delivery Semantics
A cursor identifies the position from which a client wants to resume. It might be an increasing database ID, an event sequence number, or an opaque continuation token.
The cursor must correspond to an ordering model that the event store can reproduce. Random UUIDs are useful event identifiers but do not automatically provide a meaningful ordering.
Clients should advance the cursor only after they have successfully handled the returned events. If processing fails, repeating the same cursor may deliver duplicates, so client-side processing should be idempotent when necessary.
For distributed delivery guarantees and duplicate handling, see Message Delivery Guarantees: At-Most-Once vs At-Least-Once vs Exactly-Once.
Implementing Long Polling with FastAPI
FastAPI supports asynchronous request handlers, making it suitable for long polling when the waiting operation does not block the event loop.
The central rule is simple: do not occupy a worker thread with repeated blocking sleeps while waiting for an event. Use asynchronous primitives or a message notification mechanism instead.
Minimal asyncio Example
The following example demonstrates the basic waiting behavior using an asyncio.Condition. It is intentionally process-local and does not provide durable event storage.
import asyncio
from fastapi import FastAPI, Request
app = FastAPI()
condition = asyncio.Condition()
version = 0
@app.get("/events")
async def poll_events(
request: Request,
after: int = 0,
):
global version
async with condition:
if version <= after:
try:
await asyncio.wait_for(
condition.wait_for(
lambda: version > after
),
timeout=25,
)
except TimeoutError:
return {"events": [], "cursor": after}
return {
"events": [{"version": version}],
"cursor": version,
}
@app.post("/events")
async def publish_event():
global version
async with condition:
version += 1
condition.notify_all()
return {"version": version}
This example illustrates how a request can wait without blocking the event loop. The condition lock also prevents a missed wake-up between checking the version and registering the wait.
However, it returns only the latest version, not every event. Multiple application workers would maintain independent state, and process restarts would reset the counter. It is useful for understanding the mechanism, not for reliable event delivery.
Why Asynchronous Waiting Matters
With asynchronous waiting, a FastAPI worker can suspend a pending request while continuing to handle other requests. The suspended coroutine retains some memory and connection state, but it does not require a dedicated thread to remain blocked.
That does not mean long polling has zero cost. Each pending request consumes a connection, request context, memory, and potentially file descriptors.
For more detail about asynchronous execution and event loops, see Python Concurrency: Threads, Async, Await, and Event Loops.
Production Design Example
Consider a notification service used by a SaaS application. Users need to receive new notifications within a few seconds, but updates are relatively infrequent. The service runs across multiple application instances behind a load balancer.
A production implementation needs durable notification storage, efficient wake-up signaling, authentication, and reliable cursor-based recovery.
Architecture and Data Flow
One practical design uses PostgreSQL as the authoritative notification store and Redis Pub/Sub as a low-latency wake-up mechanism.
- A business operation creates a notification in PostgreSQL.
- After the transaction commits, the application publishes a wake-up signal to Redis.
- Application instances subscribed to the relevant notification channel wake pending long-poll requests.
- Each awakened request queries PostgreSQL for events newer than its cursor.
- The server returns available events and the latest cursor.
- If no events are available, the request continues waiting until its deadline.
Redis Pub/Sub is used only to notify waiting processes that new data may exist. It is not the durable event store.
If a Pub/Sub message is lost, the event remains in PostgreSQL and can be retrieved during the next database check or client reconnection.
To avoid missing events during subscription setup, the receiver must subscribe before checking for new events or perform a second authoritative check after subscribing.
Scaling and Failure Handling
At 20,000 concurrently connected clients, the service may have approximately 20,000 pending long-poll requests, even if almost no notifications are being generated.
Each application instance must have sufficient connection capacity, memory, and file descriptors. Load balancers and reverse proxies must also support the required number of open connections.
If a notification arrives for one user, broadcasting a wake-up signal to every connected client creates unnecessary work. Per-user or sharded notification channels can reduce this overhead, although they increase subscription-management complexity.
Redis failures should not permanently prevent clients from receiving events. A bounded polling timeout followed by a fresh database check provides a recovery path, even when wake-up notifications are temporarily unavailable.
For high-availability networking considerations, see Designing Highly Available Network Architectures.
Ready-to-Use Example
The following implementation provides a working FastAPI long-polling service with Redis-backed event storage and Pub/Sub notifications. Multiple FastAPI processes can share the same Redis instance, and clients can resume from a cursor after reconnecting.
This example uses a Redis Stream as the event store and Redis Pub/Sub as a wake-up signal. The stream provides ordered event IDs, while Pub/Sub reduces the need for repeated stream queries.
Project Setup
Install the required dependencies:
pip install fastapi uvicorn redis httpx
Start Redis locally:
docker run --rm -d \
--name long-poll-redis \
-p 127.0.0.1:6379:6379 \
redis:7-alpine
The application will use a Redis Stream named notifications:events and a Pub/Sub channel named notifications:wakeup.
FastAPI Server
Save the following as app.py. The endpoint checks the stream for new events, subscribes to wake-up notifications, and rechecks the stream to avoid the subscription race condition.
import asyncio
import json
import os
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query
from redis.asyncio import Redis
from redis.exceptions import RedisError
REDIS_URL = os.getenv(
"REDIS_URL",
"redis://localhost:6379/0",
)
STREAM_KEY = "notifications:events"
WAKEUP_CHANNEL = "notifications:wakeup"
POLL_TIMEOUT = 25.0
MAX_EVENTS = 100
@asynccontextmanager
async def lifespan(app: FastAPI):
redis = Redis.from_url(
REDIS_URL,
decode_responses=True,
socket_connect_timeout=2.0,
socket_timeout=30.0,
)
app.state.redis = redis
try:
yield
finally:
await redis.aclose()
app = FastAPI(lifespan=lifespan)
async def read_events(
redis: Redis,
after: str,
):
entries = await redis.xrange(
STREAM_KEY,
min=f"({after}",
max="+",
count=MAX_EVENTS,
)
events = []
for event_id, fields in entries:
events.append({
"id": event_id,
"type": fields["type"],
"data": json.loads(fields["data"]),
})
return events
@app.get("/notifications")
async def poll_notifications(
after: str = Query(default="0-0"),
):
redis = app.state.redis
deadline = time.monotonic() + POLL_TIMEOUT
try:
events = await read_events(redis, after)
if events:
return {
"events": events,
"cursor": events[-1]["id"],
}
async with redis.pubsub() as pubsub:
await pubsub.subscribe(WAKEUP_CHANNEL)
# Recheck after subscribing to avoid missed wake-ups.
events = await read_events(redis, after)
if events:
return {
"events": events,
"cursor": events[-1]["id"],
}
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
return {
"events": [],
"cursor": after,
}
message = await pubsub.get_message(
ignore_subscribe_messages=True,
timeout=min(remaining, 1.0),
)
if message is not None:
events = await read_events(redis, after)
if events:
return {
"events": events,
"cursor": events[-1]["id"],
}
except RedisError:
raise HTTPException(
status_code=503,
detail="Notification service unavailable",
)
@app.post("/notifications")
async def publish_notification(
message: str,
):
redis = app.state.redis
try:
event_id = await redis.xadd(
STREAM_KEY,
{
"type": "notification.created",
"data": json.dumps({
"message": message,
}),
},
maxlen=10000,
approximate=True,
)
await redis.publish(
WAKEUP_CHANNEL,
event_id,
)
return {"event_id": event_id}
except RedisError:
raise HTTPException(
status_code=503,
detail="Notification service unavailable",
)
The stream is authoritative for this demonstration. Pub/Sub only wakes pending requests, and the second stream read after subscribing closes the common check-then-subscribe race.
Stream trimming keeps memory usage bounded, but it also limits how far back clients can replay events. Production systems should define retention policies and return an explicit cursor-expired response when a client requests events older than the retained history.
The example also uses a shared stream for simplicity. Real multi-tenant applications must authenticate clients and enforce access controls so users cannot read events belonging to other accounts.
Long-Polling Client
A long-polling client should maintain its cursor across requests, handle empty responses normally, and back off after transient failures.
Save the following as client.py:
import asyncio
import random
import httpx
BASE_URL = "http://127.0.0.1:8000"
async def listen():
cursor = "0-0"
failures = 0
async with httpx.AsyncClient(
timeout=httpx.Timeout(35.0),
) as client:
while True:
try:
response = await client.get(
f"{BASE_URL}/notifications",
params={"after": cursor},
)
response.raise_for_status()
payload = response.json()
for event in payload["events"]:
print(
event["id"],
event["type"],
event["data"],
)
cursor = payload["cursor"]
failures = 0
except (
httpx.RequestError,
httpx.HTTPStatusError,
) as exc:
failures += 1
delay = random.uniform(
0,
min(30.0, 2 ** min(failures, 5)),
)
print(
f"Polling failed: {exc}. "
f"Retrying in {delay:.1f}s"
)
await asyncio.sleep(delay)
if __name__ == "__main__":
asyncio.run(listen())
The HTTP client timeout is longer than the server's 25-second long-poll timeout. Otherwise, the client could repeatedly cancel valid requests before the server finishes waiting.
For a production consumer, cursor updates should occur only after successful event processing. Persisting the cursor locally or in a database allows the consumer to resume after a process restart.
Running and Testing
Start the FastAPI application:
uvicorn app:app --host 127.0.0.1 --port 8000
Start the client in another terminal:
python client.py
Publish a notification:
curl -X POST \
"http://127.0.0.1:8000/notifications?message=Hello"
The waiting client should receive the event shortly after publication, print its ID and payload, and immediately establish another long-poll request.
To verify shared state across multiple application instances, start a second FastAPI process on port 8001 using the same Redis instance. Publish events through either process and poll through the other.
The Redis Stream makes events visible to both instances, while Pub/Sub notifies subscribed requests that they should check for new data.
Common Mistakes
Long polling often works correctly with one client and one application process but behaves differently under concurrency, load balancers, or partial failures. The following problems are common in production implementations.
Blocking Application Workers
Using synchronous sleeps or blocking I/O inside request handlers can exhaust available worker threads and reduce throughput. Use asynchronous waiting and ensure database or Redis operations do not block the event loop.
Missing Events Between Requests
Events can arrive between the initial database check and listener registration, or while a client is reconnecting. Use durable cursors and recheck authoritative state after subscribing to notifications.
Mismatched Timeouts
If a reverse proxy closes idle connections after 15 seconds while the application waits for 30 seconds, clients may see repeated connection failures. Configure compatible deadlines across clients, proxies, load balancers, and application servers.
Immediate Reconnection During Outages
Thousands of clients retrying simultaneously can overwhelm a recovering service. Apply bounded exponential backoff with jitter after network failures and server errors.
Ignoring Concurrent Connection Limits
Each pending long-poll request consumes resources even when no events are available. Measure connection counts, memory usage, file descriptors, and request cancellation behavior before scaling to large client populations.
Production Checklist
A reliable long-polling service needs predictable connection lifetimes, recoverable event delivery, and bounded resource consumption.
- Timeouts: Set application poll deadlines below relevant proxy and client timeouts.
- Async I/O: Use nonblocking waits and avoid blocking the event loop.
- Cursors: Support event replay after reconnects and process restarts.
- Race conditions: Recheck authoritative state after registering event notifications.
- Connection limits: Monitor concurrent requests, memory, and file descriptor usage.
- Retries: Apply backoff and jitter after failures, while reconnecting promptly after normal empty responses.
- Security: Authenticate clients and isolate events by user or tenant.
- Observability: Track active polls, timeout rates, event delivery latency, errors, and reconnect frequency.
Frequently Asked Questions
Long polling is straightforward conceptually, but several details affect whether it is suitable for a particular application.
Is Long Polling Real-Time?
Long polling provides near-real-time updates because the server can respond shortly after an event occurs. It does not guarantee immediate delivery, and latency depends on event detection, scheduling, network conditions, and reconnection behavior.
Can Long Polling Replace WebSockets?
For infrequent server-to-client updates, long polling can be a practical alternative. WebSockets are generally better suited to frequent bidirectional messaging and applications that need persistent interactive connections.
Does Long Polling Consume More Server Resources?
Long polling reduces unnecessary repeated requests but keeps requests open longer. This increases concurrent connection counts and associated memory and file descriptor usage. Asynchronous servers can manage many pending requests efficiently, but capacity is not unlimited.
Does Long Polling Work with HTTP/2?
Yes. Long polling uses ordinary HTTP requests and works over HTTP/1.1, HTTP/2, and HTTP/3 when supported by the client and server infrastructure.
HTTP/2 multiplexing can reduce the number of underlying TCP connections, but each outstanding request still consumes stream and application resources.
Can Long Polling Run on Serverless Platforms?
It can, but platform-specific request duration limits and billing models matter. Holding requests open may consume execution concurrency or incur costs even when little application work is being performed.
For AWS Lambda, long polling must fit within the Lambda execution timeout and the timeout constraints of the HTTP integration in front of it. An event-driven push mechanism or short polling may be more economical for some serverless workloads.
Conclusion
Long polling allows applications to deliver near-real-time updates using ordinary HTTP requests. Instead of continuously requesting unchanged data, clients wait for the server to respond when new information becomes available or a timeout occurs.
Its simplicity makes it attractive for notifications, job status tracking, and systems that cannot easily adopt WebSockets or SSE. However, production reliability depends on asynchronous waiting, durable cursors, timeout coordination, connection limits, and safe reconnection behavior.
Key Takeaway: Long polling is an effective middle ground between frequent short polling and persistent streaming connections. It works best when updates are relatively infrequent and the system can tolerate repeated HTTP requests while maintaining reliable event recovery.
More Articles to Read
These related articles cover communication protocols, API reliability, and asynchronous processing patterns.
Comments (0)