# Polling, long polling o SSE: ¿qué elegir en Python?

> ¿Polling, long polling o Server-Sent Events? Cómo funcionan, su coste real, la caché HTTP y la recuperación tras un corte, con ejemplos en FastAPI.

- URL: https://dev-flow.io/es/posts/polling-long-polling-sse/
- Date: 2026-10-05
- Author: Anthony
- Language: es
- Categories: Architecture
- Tags: Python, Architecture, Api, Fastapi, Sse
- Translations: [fr](https://dev-flow.io/posts/polling-long-polling-sse/index.md), [en](https://dev-flow.io/en/posts/polling-long-polling-sse/index.md)

---


Un pedido cambia de estado, una exportación termina, llega un mensaje. El servidor lo sabe, el navegador no. Polling, long polling y Server-Sent Events (SSE) responden todos a esta misma pregunta, y se confunden a menudo porque el resultado en pantalla se parece.

Lo que los separa es **quién desencadena el envío** y **cuánto cuesta el silencio**, cuando nada cambia. Veamos cómo funciona cada uno, cuánto cuesta realmente y por qué la solución más sencilla suele ser la correcta.

**En resumen:**

- **Polling**: simple y cacheable, si unos segundos de retraso son aceptables.
- **Long polling**: reactivo sin flujo continuo, útil detrás de un proxy que hace buffering.
- **SSE**: la opción adecuada para enviar eventos frecuentes a un navegador.

## Polling: el cliente vuelve a preguntar

El cliente consulta al servidor a intervalos regulares. Nada más, y esa es su gran fortaleza: una ruta HTTP normal, workers clásicos y ninguna conexión larga.

```python
import hashlib
import json
from typing import Annotated

from fastapi import FastAPI, Header, Response, status

app = FastAPI()


@app.get("/orders/{order_id}/status")
async def order_status(
    order_id: int,
    response: Response,
    if_none_match: Annotated[str | None, Header()] = None,
):
    current = await load_status(order_id)
    body = json.dumps(current, sort_keys=True).encode()
    etag = f'"{hashlib.sha256(body).hexdigest()[:16]}"'

    if if_none_match == etag:
        return Response(status_code=status.HTTP_304_NOT_MODIFIED, headers={"ETag": etag})

    response.headers["ETag"] = etag
    response.headers["Cache-Control"] = "no-cache"
    return current
```

El `ETag` evita reenviar un cuerpo idéntico: si nada ha cambiado, el servidor responde `304` sin contenido. Atención a lo que esto ahorra realmente. El ancho de banda disminuye, pero la petición se ha hecho y `load_status` se ha ejecutado. Si esa lectura es pesada, es ella la que hay que aligerar, por ejemplo con una [vista materializada](/es/posts/django-vistas-materializadas-cache/).

En el cliente, es preferible `setTimeout` a `setInterval`. Con `setInterval`, las peticiones lentas se solapan cuando el servidor tiene problemas, en el peor momento:

```javascript
async function poll() {
  const response = await fetch("/orders/42/status");
  if (response.ok) renderStatus(await response.json());
  setTimeout(poll, 5000 + Math.random() * 1000);
}
```

El navegador gestiona solo el `If-None-Match` mediante su caché HTTP, y entrega un `200` a tu código aunque el servidor haya respondido `304`. El retardo aleatorio evita que todos los clientes lleguen en el mismo segundo.

El polling tiene una ventaja que los otros dos no tienen: **la caché compartida**. Para un dato idéntico para todos, un `Cache-Control: public, max-age=5` en una CDN convierte miles de peticiones en una sola hacia tu servidor cada cinco segundos.

Su defecto es matemático. Con un intervalo de 5 segundos, el retraso medio es de 2,5 segundos, y 10 000 clientes generan 2000 peticiones por segundo, aunque nada cambie. Reducir el retraso es multiplicar la carga.

## Long polling: el servidor retiene su respuesta

El long polling mantiene una petición abierta hasta que hay algo que decir. El cliente envía su posición, el servidor espera, responde en cuanto llega un evento y el cliente vuelve a empezar de inmediato.

```python
from typing import Annotated

from fastapi import FastAPI, Query, Response, status
from redis.asyncio import Redis

app = FastAPI()
redis = Redis.from_url("redis://localhost:6379", decode_responses=True)


@app.get("/orders/{order_id}/updates")
async def order_updates(
    order_id: int,
    after: Annotated[str, Query(pattern=r"^\d+-\d+$")] = "0-0",
    wait: Annotated[int, Query(ge=1, le=25)] = 25,
):
    stream = f"orders:{order_id}:events"
    batches = await redis.xread({stream: after}, block=wait * 1000, count=100)
    if not batches:
        return Response(status_code=status.HTTP_204_NO_CONTENT)

    _, messages = batches[0]
    return {"events": [fields for _, fields in messages], "next": messages[-1][0]}
```

```javascript
async function longPoll(after) {
  try {
    const response = await fetch(`/orders/42/updates?after=${after}`);
    if (response.status === 200) {
      const { events, next } = await response.json();
      events.forEach(renderEvent);
      after = next;
    }
    longPoll(after);
  } catch {
    setTimeout(() => longPoll(after), 2000 + Math.random() * 3000);
  }
}
```

El cursor `after` no es un detalle. Entre dos peticiones, el cliente ya no escucha: sin una posición desde la que continuar, cualquier evento publicado en ese momento se perdería. Hace falta por tanto una fuente que conserve el historial, lo que descarta una simple difusión. Es toda la diferencia entre [Redis Pub/Sub y Streams](/es/posts/redis-pub-sub-vs-streams/).

El parámetro `wait` se limita a 25 segundos por una razón práctica: los proxies y balanceadores cortan las peticiones que permanecen demasiado tiempo en silencio (Nginx a los 60 segundos por defecto, algunas plataformas ya a los 30). Es mejor responder `204` limpiamente y dejar que el cliente vuelva a empezar.

El long polling también tiene sus propios costes. Bajo WSGI, cada petición en espera inmoviliza un worker: exige un servidor asíncrono. Y cada evento cuesta un viaje de ida y vuelta HTTP completo, cabeceras incluidas. El parámetro `count` limita el daño agrupando las ráfagas, pero el caudal sigue siendo inferior al de un flujo continuo.

Sigue siendo valioso donde un flujo continuo pasa mal: detrás de un proxy que almacena en búfer, o para un cliente que no necesita navegador. La API de bots de Telegram expone así `getUpdates`, long polling con un `offset` que cumple exactamente la función de nuestro cursor.

## SSE: una sola respuesta que no termina

SSE lleva la idea hasta el final: en lugar de responder y esperar una nueva petición, el servidor mantiene abierta la misma respuesta y escribe en ella cada evento con el formato `text/event-stream`.

```text
Polling       Cliente → ?  Servidor → no  |  Cliente → ?  Servidor → no  |  Cliente → ?  Servidor → sí
Long polling  Cliente → ?  ........ Servidor → sí  |  Cliente → ?  .... Servidor → sí
SSE           Cliente → ?  ........ Servidor → sí ...... sí .. sí ........ sí
```

El navegador ofrece gratis lo que el long polling te obliga a escribir a mano: la API `EventSource` se reconecta sola y reenvía la última posición en la cabecera `Last-Event-ID`. La [implementación con FastAPI y Django, y la comparación con WebSocket](/es/posts/server-sent-events-vs-websocket/), tienen su propio artículo.

A cambio, cada cliente mantiene una conexión abierta, la caché HTTP deja de servir y cada proxy del camino debe dejar pasar el flujo sin retenerlo.

## El coste real, en cifras

Tomemos 10 000 clientes y un evento por cliente y por minuto, es decir, unos 167 eventos por segundo en total. Es un orden de magnitud, suponiendo un polling cada 5 segundos y un long polling limitado a 25 segundos.

| | Polling (5 s) | Long polling | SSE |
|---|---|---|---|
| Peticiones por segundo | 2000 | Unas 500 | Casi ninguna, salvo reconexiones |
| Peticiones útiles | 1 de cada 12 | 1 de cada 3 | No aplica |
| Retraso medio | 2,5 s | Casi inmediato | Casi inmediato |
| Conexiones abiertas | Muy pocas | Unas 10 000 | 10 000 |
| Caché HTTP compartida | Sí | No | No |
| Servidor | WSGI o ASGI | ASGI | ASGI |
| Reconexión y reanudación | Nada que gestionar | A escribir | Nativa con `EventSource` |
| Límite HTTP/1.1 del navegador | Sin efecto notable | Ocupa una conexión | Ocupa una conexión |

La tabla muestra que la verdadera comparación no se limita al número de peticiones. El polling consume CPU y red de forma permanente, mientras que los otros dos consumen conexiones y memoria. Y 10 000 conexiones abiertas pesan muy poco mientras las gestione un bucle asíncrono, pero complican cada despliegue: al reiniciar, todos los clientes vuelven al mismo tiempo.

## Polling, long polling o SSE: cómo elegir

Parte de la frescura que necesitan los datos y toma la opción más sencilla que la ofrezca.

**El polling** conviene cuando unos segundos de retraso no molestan a nadie, cuando los datos son compartidos y cacheables o cuando la infraestructura no debe cambiar. Un panel que se actualiza cada 30 segundos no necesita nada más.

**El long polling** conviene cuando hace falta reactividad pero un flujo continuo no pasa: proxies corporativos que almacenan en búfer, clientes que no son navegadores, integraciones sencillas en el servidor.

**SSE** conviene en cuanto un navegador debe recibir eventos frecuentes con poco retraso: notificaciones, progreso, respuestas de LLM mostradas a medida que se generan.

Y si el cliente también debe emitir de forma continua, se deja este trío por [WebSocket](/es/posts/server-sent-events-vs-websocket/).

Un último consejo: en todos los casos, conserva una lectura completa del estado. Una reanudación puede fallar y un cursor puede ser demasiado antiguo. El flujo dice «algo ha cambiado», y la lectura clásica sigue siendo la fuente de verdad.

## Conclusión

Polling, long polling y SSE no son tres niveles de calidad, sino tres formas de repartir un coste. El polling paga en cada comprobación, el long polling en cada evento, y SSE una vez al conectarse y luego, con el tiempo, en conexiones que mantener.

La pregunta correcta no es, por tanto, «¿cuál es la técnica más moderna?», sino «¿qué frescura necesito realmente y qué estoy dispuesto a operar para conseguirla?».

