wandres.dev
PRIMITIVOS DE REACTIVIDAD · observer y pub/sub

Push vs pull: quién tiene el control del tiempo

La pregunta más profunda de la reactividad no es cómo se propaga un cambio, sino quién decide cuándo: ¿la fuente empuja el valor a sus dependientes, o estos preguntan cuando lo necesitan? Ese eje separa a iteradores de observables, es una dualidad matemática exacta, y su síntesis —el modelo híbrido push-pull— es la clave del diseño de los signals modernos.

⏱ 16 min

Toda relación reactiva tiene un dueño del tiempo. O la fuente decide cuándo hay un valor nuevo y lo empuja hacia quienes dependen de ella —push—, o los dependientes deciden cuándo lo necesitan y lo piden —pull—. Ese único eje explica la diferencia entre un evento y un iterador, entre RxJS y un generador, y por qué los sistemas reactivos de 2026 convergen todos en un híbrido que toma prestado lo mejor de ambos.

🎯 Al terminar esta lección sabrás
  • Definir el eje push, en el que la fuente empuja, frente a pull, en el que el dependiente pregunta.
  • Reconocer la dualidad matemática entre iteradores (pull) y observables (push).
  • Entender el modelo híbrido push-pull de los signals modernos y por qué gana.
  • Elegir el modelo según quién deba controlar el momento en que se produce el valor.

El eje: quién decide cuándo

Los tres primeros temas del nivel tratan del mecanismo de propagación; este trata del control. Cuando aparece un valor nuevo, alguien tiene que decidir en qué instante fluye. En el modelo push, la fuente manda: cuando tiene algo, invoca a sus dependientes —el notify del Observer, el dispatchEvent de un emitter, el next que RxJS empuja a sus suscriptores—. En el modelo pull, el consumidor manda: la fuente no hace nada hasta que alguien le pide el siguiente valor —una llamada a función, un next de iterador, la lectura de un getter—. Es, en el fondo, una cuestión de inversión de control sobre el tiempo.

Y no es un matiz de implementación. De esa única decisión se derivan casi todas las propiedades observables de una fuente: si es perezosa o ansiosa, si tolera a un consumidor lento, si puede exponer estados a medio propagar y hasta la forma de su API. Por eso conviene tratar la pregunta como la primera que le haces a cualquier librería reactiva, antes que su sintaxis concreta: quién manda sobre el tiempo predice casi todo lo demás.

Pull: el consumidor manda

En pull, la evaluación es perezosa: nada se produce hasta que alguien lo reclama. Funciones, iteradores y generadores son sus encarnaciones. Un generador puede describir una secuencia infinita sin coste, porque solo calcula cada término cuando se le pide:

function* naturales() {
  let n = 0;
  while (true) yield n++; // no produce nada hasta que se hace pull
}

const it = naturales();
it.next().value; // 0  — el consumidor decide cuándo avanzar
it.next().value; // 1

La ventaja del pull es que el backpressure es natural: consumes a tu propio ritmo, y la fuente no puede abrumarte porque no produce sin que preguntes. Su desventaja es simétrica: si el dato cambia por su cuenta, no te enteras hasta que vuelves a preguntar, así que para detectar cambios externos tendrías que sondear en bucle, que es justo lo que el push evita.

Los dos sabores del pull

El pull tiene dos encarnaciones en JavaScript. El síncrono es el protocolo de iteración —Symbol.iterator y next— que recorre arrays, mapas y generadores bajo demanda, un elemento cada vez que se pide. El asíncrono es Symbol.asyncIterator, que consumes con for await y sirve para fuentes que tardan en producir cada valor: leer un fichero por trozos, paginar una API, drenar una cola.

// pull asíncrono: cada vuelta del bucle hace un next bajo demanda
for await (const linea of leerPorLineas(fichero)) {
  procesar(linea); // la fuente no adelanta trabajo: espera a que pidas la siguiente
}

Ambos sabores comparten la esencia del pull —nada avanza hasta que el consumidor reclama el siguiente elemento— y por eso ambos regalan el backpressure sin pedir nada a cambio: el productor jamás corre por delante del consumidor.

Push: la fuente manda

En push, la evaluación es ansiosa respecto al consumidor: la fuente emite cuando ella quiere y el consumidor solo reacciona. Es el modelo obligado para todo lo que no controlas —clics, mensajes de un socket, el tictac de un reloj—, porque esos sucesos ocurren cuando ocurren y no tiene sentido preguntar por ellos.

const reloj = new EventTarget();
setInterval(() => reloj.dispatchEvent(new Event("tick")), 1000);
reloj.addEventListener("tick", () => render()); // la fuente empuja; tú reaccionas

Su desventaja es el reverso de su virtud: la fuente puede empujar más de lo necesario —recalcular en cada emisión aunque nadie mire el resultado— y puede exponer glitches, estados intermedios inconsistentes cuando varios valores derivados se actualizan en cascada y alguien observa uno a medio propagar. Y el backpressure es difícil: un productor veloz inunda a un consumidor lento.

Conviene ver la Promise como el caso degenerado del push: empuja exactamente un valor —o un error— una sola vez y termina. Un Observable generaliza esa idea a cero, uno o infinitos valores repartidos en el tiempo; de ahí que RxJS lo describa como una promesa de muchos valores. El salto de uno a muchos es justo lo que obliga a pensar en completado, error y backpressure, dimensiones que una Promise de un único valor nunca necesita.

Dentro del propio push hay un segundo matiz: una fuente puede ser fría —cada suscriptor arranca su propia ejecución desde el principio, como un Observable que relanza su productor en cada subscribe— o caliente —todos comparten una única ejecución en curso, como un EventTarget o un socket—. La diferencia decide si dos suscriptores ven la misma secuencia o cada uno la suya, y es una fuente inagotable de sorpresas para quien no la tiene presente.

ℹ️
Una dualidad matemática, no una metáfora

Erik Meijer lo formuló con precisión: el par Iterable/Iterator (pull) y el par Observable/Observer (push) son duales exactos. Invierte el sentido de las flechas y uno se convierte en el otro. En pull, el consumidor llama a next y recibe un valor de vuelta; en push, la fuente llama a update y entrega un valor. Es el mismo intercambio con la dirección del control invertida. Reconocer esta dualidad es dejar de ver push y pull como dos mundos y verlos como las dos caras de una misma moneda.

flowchart LR
subgraph PULL
  C1[Consumidor] -->|pide next| F1[Fuente perezosa]
  F1 -->|entrega valor| C1
end
subgraph PUSH
  F2[Fuente ansiosa] -->|empuja valor| C2[Consumidor]
end
style F1 fill:#89b4fa,color:#11111b
style C1 fill:#a6e3a1,color:#11111b
style F2 fill:#89b4fa,color:#11111b
style C2 fill:#a6e3a1,color:#11111b
Dimensión Pull Push
Quién controla el tiempo el consumidor la fuente
Evaluación perezosa ansiosa
Backpressure natural difícil
Detectar cambios externos requiere sondeo inmediato
Encaje ideal cómputo bajo demanda eventos que no controlas
Ejemplo típico iterador, generador, getter evento, socket, stream

La moraleja de la tabla es que ninguno gana en abstracto. El pull encaja cuando tú controlas el ritmo y el dato solo cambia si lo pides; el push, cuando el dato cambia por causas ajenas a ti y no quieres perderte nada. La incomodidad nace de forzar uno donde tocaba el otro: sondear en bucle una fuente que ya empujaba, o suscribirte a algo que solo querías leer una vez.

El híbrido push-pull: lo mejor de los dos

Los signals modernos no eligen bando: son push-pull. Al escribir en un signal se empuja hacia el grafo una simple marca de “desactualizado”, barata y sin recálculo alguno. Solo cuando alguien lee un valor derivado se tira de él, recalculándolo de forma perezosa y únicamente si su marca dice que está sucio. Así se ganan las dos cosas a la vez: no se malgasta cómputo —solo se recalcula lo que de verdad se lee— y no hay glitches —un derivado nunca se observa a medio actualizar, porque se reconstruye bajo demanda a partir de fuentes ya consistentes—.

En pseudocódigo la asimetría se ve nítida: escribir marca y avisa; leer comprueba y, solo si hace falta, recalcula y cachea.

precio.set(12); // push barato: marca a total como sucio, NO lo recalcula aún

total(); // pull perezoso: ve la marca, recalcula una vez y queda limpio
total(); // ya está limpio: devuelve el valor cacheado, sin recalcular

El caso clásico que esto evita es el diamante: un valor a, dos derivados b y c que dependen de a, y un cuarto d que depende de b y de c. En push puro, cambiar a puede recalcular d dos veces y dejar que alguien lo observe con b ya nuevo pero c todavía viejo: un glitch. En push-pull, d no se recalcula hasta que se lee, y para entonces b y c ya son consistentes, de modo que el glitch es imposible por construcción.

Esa síntesis es la razón por la que la propuesta de Signals de TC39, Solid, Vue y Angular convergen en el mismo diseño. La invalidación viaja en push para reaccionar al instante a los cambios; la evaluación ocurre en pull para no calcular de más ni observar estados a medias. Es el eje de esta lección resuelto en ingeniería: en lugar de escoger un bando, se separan las dos preguntas que parecían una sola —cuándo avisar de que algo caducó y cuándo producir el valor nuevo— y a cada una se le da la respuesta que le conviene.

Dónde vive cada modelo en 2026

  • Puro pull: funciones, getters, iteradores y generadores; también revalidar bajo demanda con TanStack Query.
  • Puro push: EventTarget, el EventEmitter de Node, los observables de RxJS, los server-sent events y los websockets.
  • Push-pull: los signals de Solid, Angular, Vue y Preact, las runes de Svelte 5 y la propuesta de TC39; también MobX, pionero del autoseguimiento perezoso.

Si sabes colocar una herramienta en una de estas tres casillas, ya predices su pereza, su backpressure y su tendencia a los glitches sin abrir su documentación.

Convertir de un modelo a otro es una técnica cotidiana. Del push al pull, por ejemplo, para consumir eventos a tu propio ritmo:

import { on } from "node:events";

// el bucle no adelanta hasta terminar cada vuelta: pull sobre una fuente push
for await (const [payload] of on(emisor, "cambio")) {
  await procesarLento(payload);
}

Y una advertencia para no idealizar el híbrido: push-pull brilla para el estado derivado dentro de una aplicación, pero no sustituye a los otros dos donde son insustituibles. Un socket sigue siendo push puro en su frontera y recorrer un fichero enorme sigue siendo pull perezoso. La madurez no es usar signals para todo, sino saber en qué modelo vive cada tramo del sistema y convertir de uno a otro en los bordes.

Push vs pull es la pregunta de quién es dueño del tiempo

Detrás de cada API reactiva que elegirás hay una respuesta, explícita o no, a una sola pregunta: cuando aparece un valor nuevo, ¿quién decide el instante en que fluye? El pull entrega ese poder al consumidor y con él la pereza y el backpressure gratis, a cambio de ceguera ante lo que cambia solo. El push se lo da a la fuente y con él la reacción inmediata a lo externo, a cambio del riesgo de calcular de más y de los glitches. Durante años el ecosistema pareció obligado a escoger —los observables abrazaron el push, los iteradores el pull— hasta que se entendió que el dilema era falso. Los signals demostraron que puedes separar las dos decisiones que parecían una: empujar la señal de que algo caducó, que debe ser inmediata para que nada quede obsoleto, y tirar del valor recalculado, que debe ser perezosa para no trabajar en balde ni exponer estados a medias. Push para la invalidación, pull para la evaluación. Quien interioriza este eje deja de memorizar APIs y empieza a predecirlas: ante cualquier librería nueva sabe preguntar quién controla el tiempo, y de esa única respuesta deduce su pereza, su backpressure, su propensión a los glitches y su encaje. No es un detalle de implementación; es la columna vertebral conceptual sobre la que se apoya el salto a la reactividad automática que cierra el nivel.

⚔️ Empuja, tira y combina
  1. Escribe un generador que produzca una secuencia infinita y confirma que no calcula nada hasta que llamas a next: eso es pull perezoso.
  2. Monta un EventTarget que emita en un intervalo y observa que reaccionas sin pedir nada: eso es push ansioso.
  3. Convierte el push en pull con events.on(emitter, nombre) de Node y consúmelo con for await; explica qué has invertido.
  4. Describe con tus palabras cómo un signal usa push para avisar de que un valor caducó y pull para recalcularlo, y por qué esa separación evita a la vez el cómputo inútil y los glitches.