El polling tiene mala fama: suena a chapuza al lado de WebSockets o Mercure. Pero en muchos casos sigue siendo la opción más sencilla y la que mejor funciona. Te explico qué es, cuándo usarlo y qué alternativas hay cuando no encaja.
Explicación
Hacer polling es que el navegador pregunte al servidor cada cierto tiempo: «¿hay algo nuevo?». El servidor responde con lo que tenga y, pasados unos segundos, el navegador vuelve a preguntar.
async function checkStatus() {
const response = await fetch('/reports/42/status');
const data = await response.json();
if (data.finished) {
window.location = data.url; // ya está: dejamos de preguntar
return;
}
setTimeout(checkStatus, 3000); // si no, volvemos a preguntar en 3 segundos
}
checkStatus();
Y en el servidor, una ruta pequeña que solo devuelve el estado:
#[Route('/reports/{id}/status')]
public function status(Report $report): JsonResponse
{
return $this->json([
'finished' => $report->isFinished(),
'url' => $report->getUrl(),
]);
}
Eso es todo: peticiones HTTP normales. Sin conexiones abiertas, sin servidores especiales y sin librerías.
Otro ejemplo: comentarios nuevos
Una página muestra los comentarios de un post y queremos que los nuevos aparezcan solos, sin recargar. La idea: el navegador recuerda el id del último comentario que tiene y pregunta «¿hay alguno después de este?».
Al pintar la página, guardamos ese último id en la lista:
<ul id="comments" data-last-id="{{ comments is empty ? 0 : (comments|last).id }}">
{% for comment in comments %}
<li>{{ comment.author }}: {{ comment.text }}</li>
{% endfor %}
</ul>
La ruta devuelve solo los comentarios con un id mayor. Si no hay ninguno, una lista vacía:
#[Route('/posts/{id}/comments')]
public function newComments(Post $post, Request $request, CommentRepository $repository): JsonResponse
{
$after = $request->query->getInt('after');
$comments = $repository->createQueryBuilder('c')
->where('c.post = :post AND c.id > :after')
->setParameter('post', $post)
->setParameter('after', $after)
->orderBy('c.id')
->getQuery()
->getResult();
return $this->json(array_map(fn (Comment $c) => [
'id' => $c->getId(),
'author' => $c->getAuthor(),
'text' => $c->getText(),
], $comments));
}
Y el JavaScript pregunta cada 10 segundos y añade al final lo que llegue:
const list = document.querySelector('#comments');
let lastId = Number(list.dataset.lastId);
async function checkComments() {
try {
const response = await fetch(`/posts/7/comments?after=${lastId}`);
const newComments = await response.json();
for (const comment of newComments) {
const li = document.createElement('li');
li.textContent = `${comment.author}: ${comment.text}`;
list.append(li);
lastId = comment.id; // el siguiente fetch pedirá a partir de aquí
}
} finally {
setTimeout(checkComments, 10000); // aunque falle, volvemos a preguntar
}
}
setTimeout(checkComments, 10000);
Dos detalles:
- El
finally. Si un día falla la petición (se corta la wifi un momento), sin él no se volvería a preguntar nunca más. Con él, el bucle sigue pase lo que pase. - Una sola pregunta, no dos. Podríamos preguntar primero «¿hay nuevos?» y luego «¿cuáles?», pero no hace falta: si no hay nada, la lista vacía cuesta lo mismo que un «no». Y si los hay, te ahorras una petición.
textContent, noinnerHTML. El texto lo ha escrito un usuario: coninnerHTML, un comentario con<script>se ejecutaría en la página de todos los lectores.
Cuándo sigue siendo mejor usarlo
- Esperar a que termine una tarea. El usuario pide un informe, lo mandas a una cola con Messenger y le enseñas un «Generando…». Preguntar cada pocos segundos hasta que esté listo es perfecto.
- Datos que cambian poco. El estado de un pedido o las notificaciones sin leer. Si da igual enterarse unos segundos tarde, no hace falta más.
- Tu servidor es PHP normal. Con PHP-FPM, cada conexión que se queda abierta ocupa un proceso entero. Con polling, cada petición dura milisegundos y el proceso queda libre enseguida.
- Una API externa que no avisa. Si el proveedor no tiene webhooks, no queda otra que preguntarle tú cada cierto tiempo (con un comando en cron).
- Pocos usuarios. Un panel interno que usan cinco personas puede refrescar cada 10 segundos sin que nadie lo note.
Para saber si tu servidor lo aguanta, una cuenta rápida: usuarios conectados ÷ segundos entre preguntas = peticiones por segundo. 100 usuarios cada 5 segundos son 20 peticiones por segundo: nada.
Las alternativas
Cuando el polling se queda corto, estas son las opciones. Todas tienen algo en común: mantienen una conexión abierta con cada usuario, y eso necesita más infraestructura.
| Alternativa | Cómo funciona | Úsala para |
|---|---|---|
| Long polling | El servidor no responde hasta que hay algo nuevo. | Casi nada hoy: SSE hace lo mismo mejor. |
| SSE / Mercure | El servidor envía datos cuando quiere por una conexión abierta. | Avisos al instante a muchos usuarios. |
| WebSockets | Conexión abierta en los dos sentidos. | Chats, juegos, edición colaborativa. |
En Symfony, lo habitual para tiempo real es Mercure: un servidor aparte que mantiene las conexiones, para que PHP no tenga que hacerlo. Funciona muy bien, pero es una pieza más que desplegar y mantener.
Si usas polling, hazlo bien
setTimeout, nosetInterval, aunque preguntes siempre. Si una respuesta tarda más que el intervalo,setIntervallanza la siguiente petición igualmente. En los comentarios, eso son dos peticiones con el mismolastIdy el mismo comentario pintado dos veces. ConsetTimeoutal final, solo preguntas cuando ha llegado la respuesta anterior.- Que un error no pare el bucle. Pon el
setTimeouten unfinally, como en el ejemplo de los comentarios. - Si la espera tiene final, para. Como el
returndel primer ejemplo. - Pregunta menos si la pestaña está oculta.
document.hidden ? 30000 : 3000. - Que la ruta sea rápida. Devuelve un JSON pequeño, no una página entera.
- Protégela con un rate limiter. Una ruta que se llama en bucle es justo la que conviene limitar.
Y ya está
El polling no es una solución de segunda: es la más simple que funciona. Antes de montar
un servidor de tiempo real, pregúntate cuántos segundos de retraso importan de verdad. Si
la respuesta es «unos cuantos», te basta con un setTimeout y una ruta
pequeña.