¿Qué es el procesamiento por lotes (batch processing)?
El procesamiento por lotes es la práctica de tomar tareas que son demasiado lentas, frágiles o irregulares para ejecutarse mientras un usuario espera, y delegarlas a un proceso separado que se ejecuta según su propio calendario. La solicitud del usuario hace lo mínimo indispensable —validar la entrada, escribir una fila, encolar un trabajo— y retorna. Todo lo demás sucede fuera de banda.
El nombre proviene de los trabajos de la era de los mainframes que procesaban un montón de registros en una sola ejecución, y la idea no ha cambiado. En lugar de una cinta, tienes una cola. En lugar de una ventana nocturna, tienes workers procesando continuamente. Lo que permanece constante es la separación: la entidad que acepta el trabajo y la que lo ejecuta son diferentes, y hay un buffer entre ellas.
Esta separación es el objetivo principal. Permite que la ruta de la solicitud se mantenga rápida y predecible, mientras que la ruta lenta tarda lo que necesite, reintenta la operación cuando falla y escala siguiendo una curva diferente.
Por qué el trabajo pesado no debe estar en una solicitud
Una solicitud HTTP síncrona es un mal lugar para tareas lentas, debido a tres razones que se potencian entre sí.
Primero, los timeouts. Los proxies, balanceadores de carga y clientes imponen plazos límite. Una solicitud que genera un PDF de 200 páginas, llama a una API de terceros inestable y envía un correo electrónico puede superarlos fácilmente. Cuando esto sucede, el cliente ve un error aunque el trabajo se haya completado parcialmente en el servidor.
Segundo, un event loop bloqueado. Node ejecuta JavaScript en un solo hilo. Una tarea síncrona con un uso intensivo de CPU —procesamiento de imágenes, el parseo de un JSON grande, un bucle criptográfico— impide que se atiendan todas las demás solicitudes mientras se ejecuta. Incluso el trabajo asíncrono retiene recursos: una conexión abierta a la base de datos, un manejador de archivos o memoria para la respuesta.
Tercero, los reintentos son imposibles. Si el proveedor de correo devuelve un 503, un manejador inline no tiene buenas opciones. Puede hacer que toda la solicitud falle y obligar al usuario a reintentarlo, o ignorar el error y perder el correo. Una cola ofrece una tercera respuesta ante el mismo fallo: intentar de nuevo más tarde, automáticamente, sin que el usuario lo sepa.
app.post("/reports", async (req, res) => {
const report = await db.report.create({ data: { userId: req.user.id } });
await reportsQueue.add("generate", { reportId: report.id });
res.status(202).json({ id: report.id, status: "queued" });
});
El estado 202 Accepted es la respuesta honesta en este caso: el servidor ha aceptado la solicitud pero no ha terminado el trabajo. Es la estructura de cualquier endpoint de procesamiento por lotes bien implementado.
La anatomía de un job
Un job es un registro pequeño y autodescriptivo. La mayoría de las colas almacenan algo como esto:
{
"id": "welcome:user_42",
"name": "welcome",
"data": { "userId": "user_42", "to": "[email protected]", "template": "welcome" },
"status": "queued",
"attemptsMade": 0,
"maxAttempts": 5,
"runAt": 1760000000000,
"createdAt": 1759999100000
}
Cada campo tiene una razón de ser. El id hace que el job sea direccionable y, cuando se deriva del trabajo en lugar de ser aleatorio, te proporciona deduplicación de forma gratuita. El name redirige el job a un handler. El payload contiene todo lo que el worker necesita —y nada más, ya que un payload que apunta a una fila de la base de datos es más pequeño y actual que uno que la copia. El status rastrea el job a lo largo de su ciclo de vida. attemptsMade y maxAttempts gestionan los reintentos. runAt programa el trabajo diferido y recurrente.
El payload debe ser una instantánea de la intención, no un objeto vivo. Si un usuario actualiza su correo electrónico entre el momento del enqueue y la ejecución, el job debería seguir enviándose a la dirección con la que fue creado. Sin embargo, almacenar todo el registro del usuario es un error: infla la cola y queda obsoleto. Almacena ids y los pocos valores que definan el trabajo.
Payloads de trabajos y versionado
Una cola es una interfaz persistente entre dos despliegues. Un productor que ejecute la versión 1 del código puede escribir un trabajo que un worker que ejecute la versión 2 debe leer. Este es el mismo problema de compatibilidad que ocurre con una API, y es fácil ignorarlo hasta que un despliegue rompe el backlog.
Dos hábitos mantienen los payloads compatibles. Primero, añade campos en lugar de renombrarlos o eliminarlos, y asigna un valor por defecto coherente a los campos nuevos en el handler. Un worker que tolere la ausencia de locale puede procesar trabajos encolados antes de que el campo existiera. Segundo, incluye una versión en el payload cuando la estructura pueda cambiar significativamente:
await queue.add("import", { version: 2, importId, mapping });
El handler entonces bifurca la lógica basándose en version y sabe exactamente cómo interpretar el resto. Esto casi no tiene coste y convierte una categoría de incidentes post-despliegue en una simple tabla de búsqueda.
Mantén los payloads pequeños. Una cola almacena cada trabajo en espera, por lo que un payload que incluya un objeto grande se multiplica en miles de filas y ralentiza cada escaneo. Haz referencia a los datos mediante un id y deja que el worker los recupere. La única excepción es un valor que deba quedar congelado al momento de encolar —el destinatario de un email, el precio cotizado a un cliente—, el cual pertenece al payload precisamente porque no debe cambiar.
Colas, cron y disparadores basados en eventos
No todas las tareas en segundo plano necesitan una cola, y elegir el disparador equivocado puede complicar un problema sencillo.
Cron ejecuta un manejador según un horario fijo: todas las noches a las 02:00, todos los lunes a las 09:00. Es la herramienta adecuada para conciliaciones periódicas, generación de reportes y limpieza de datos. Su debilidad es que no tiene un concepto de unidades de trabajo. Un trabajo de cron que tarde más que su propio intervalo se solapará consigo mismo, y una ejecución fallida simplemente se pierde.
Los disparadores basados en eventos reaccionan a algo que acaba de suceder: llegó un webhook, se insertó una fila, un archivo aterrizó en el almacenamiento. Son inmediatos y naturales, pero no ofrecen almacenamiento intermedio (buffering), ni reintentos, ni control de presión (backpressure). Un manejador de webhook que realiza trabajo real es, en esencia, una solicitud inline disfrazada.
Las colas se sitúan entre ambos. Un trabajo es una unidad de trabajo duradera y reintentable que puede programarse para ahora o para más tarde. La mayoría de los sistemas en producción utilizan los tres: cron encola un trabajo de distribución (fan-out), los eventos encolan trabajos en respuesta a acciones del usuario y los workers vacían la cola. La regla general es que cron y los eventos deciden cuándo trabajar, y la cola decide cómo trabajar.
Productores, workers y la cola
El sistema se compone de tres roles, y mantenerlos diferenciados es lo que permite que sea mantenible.
El productor es cualquier código que añade un trabajo. Solo conoce el nombre del trabajo y la estructura del payload, nada más. Debe ser rápido y debe ser seguro llamarlo dos veces; si el productor reintenta la operación tras un timeout, no querrás que se creen dos trabajos.
La cola es un almacén ordenado y duradero. Redis con BullMQ, Amazon SQS, RabbitMQ y Google Cloud Tasks cumplen este rol. La cola persiste los trabajos a través de los reinicios, los distribuye de forma atómica, rastrea los intentos y aparta los trabajos agotados. También puedes construir una basada en una tabla de base de datos, lo cual es un comienzo razonable cuando el volumen es bajo, pero se convierte en un problema cuando deja de serlo.
El worker es un proceso de larga ejecución que extrae los trabajos y ejecuta los handlers. Está separado de la API por buenas razones: puede desplegarse en hardware optimizado para CPU, escalarse según la profundidad de la cola y reiniciarse sin perder peticiones. En BullMQ, esta separación es explícita:
import { Worker } from "bullmq";
import { connection } from "./queue.js";
const worker = new Worker(
"emails",
async (job) => {
await sendEmail(job.data);
},
{ connection, concurrency: 10 },
);
Un solo proceso puede albergar varios workers para diferentes colas, y una sola cola puede ser atendida por muchos procesos worker. La cola es el único estado compartido, que es precisamente la razón por la cual escala horizontalmente de forma tan limpia.
Eligiendo un broker
Los tres brokers con los que te encontrarás más a menudo se sitúan en diferentes puntos de un espectro entre funcionalidades y carga operativa.
Redis con BullMQ es la opción predeterminada para los equipos de Node.js. Es probable que Redis ya esté en tu stack, el cliente es maduro y BullMQ añade jobs retrasados, jobs repetibles, prioridades, rate limiting, reintentos y una UI. El inconveniente es que Redis es principalmente un almacenamiento en memoria, por lo que la durabilidad depende de cómo configures la persistencia. Un job confirmado pero que aún no se haya escrito en disco puede perderse si la instancia cae.
Amazon SQS es totalmente gestionado y tiene un throughput prácticamente ilimitado. Obtienes durabilidad y disponibilidad sin tener que ejecutar nada, pagando por solicitud. A cambio, sacrificas cierta ergonomía: la entrega retrasada está limitada, no hay un scheduler integrado para jobs tipo cron y la API es de más bajo nivel que un framework de jobs.
RabbitMQ es el más flexible. Los exchanges y las routing keys permiten que un mensaje se distribuya a muchos consumidores, y los acknowledgements por mensaje, las prioridades y los dead-letter exchanges son funcionalidades nativas. Es más pesado de operar que Redis y tiene una curva de aprendizaje más pronunciada, pero es ideal para enrutamientos complejos.
La recomendación honesta es empezar con lo que ya estés ejecutando. Una cola en Redis es mucho mejor que no tener ninguna cola por estar esperando a evaluar brokers. Cambia cuando una limitación específica —ya sea de durabilidad, throughput o enrutamiento— realmente se convierta en un problema.
Idempotencia y entrega at-least-once
El hecho más importante sobre las colas de trabajos es que la entrega es at-least-once (al menos una vez), no exactamente una vez. Un worker puede fallar después de realizar el trabajo pero antes de confirmarlo, y la cola volverá a entregar el trabajo. Un microcorte de red puede hacer que una confirmación desaparezca. El trabajo se ejecuta de nuevo.
Esto no es un bug que debas solucionar; es el contrato. La entrega exactamente una vez a través de una red es efectivamente imposible, por lo que las colas eligen at-least-once y trasladan la responsabilidad a ti. Tu handler debe ser idempotente: ejecutarlo dos veces debe producir el mismo estado final que ejecutarlo una sola vez.
Existen tres técnicas prácticas.
Idempotencia natural. Algunas operaciones ya son seguras de repetir. Establecer el estado de un usuario a active dos veces es lo mismo que hacerlo una. Eliminar una fila por id la segunda vez es una operación nula (no-op). Prefiere estas opciones siempre que sea posible.
Una clave de deduplicación. Escribe un marcador indexado por el trabajo antes de realizarlo, y omítelo si el marcador ya existe. Una restricción de unicidad (unique constraint) o un SET NX de Redis hace que la comprobación sea atómica, evitando que dos workers concurrentes ganen al mismo tiempo.
export async function handleCharge(job) {
const key = `charged:${job.data.orderId}`;
const inserted = await connection.set(key, "1", "NX", "EX", 86_400);
if (inserted === null) return { skipped: true };
await stripe.charges.create(
{ amount: job.data.amount, source: job.data.token },
{ idempotencyKey: job.data.orderId },
);
}
Claves de idempotencia del proveedor. Las pasarelas de pago y muchas otras API aceptan una clave de idempotencia. Envía el id estable del trabajo y el proveedor devolverá el resultado original en lugar de realizar el cobro dos veces. Combina siempre esto con tu propia deduplicación, ya que la clave solo protege la llamada, no la lógica circundante.
Observa que la clave de deduplicación se deriva del trabajo —el id del pedido— y no del id del trabajo. Esto es deliberado: hace que el handler sea seguro incluso si el productor encola el mismo trabajo lógico dos veces.
Reintentos con exponential backoff y jitter
Los fallos transitorios son normales. Una base de datos hace un failover, una API te aplica un rate-limit, un contenedor es reprogramado. Reintentar es la respuesta correcta, pero reintentar inmediatamente no lo es.
El exponential backoff aumenta el retraso en cada intento: aproximadamente 2s, 4s, 8s, 16s, 32s. Esto le da tiempo a una dependencia con problemas para recuperarse en lugar de seguir siendo bombardeada. El jitter añade una cantidad aleatoria a cada retraso para que muchos jobs que fallaron simultáneamente no reintenten al mismo tiempo, lo que recrearía el pico de tráfico que causó el fallo.
La mayoría de las colas soportan esto de forma declarativa. En BullMQ es una propiedad del job:
await queue.add(
"sync",
{ accountId },
{
attempts: 5,
backoff: { type: "exponential", delay: 2_000 },
},
);
Eso produce retrasos de unos 2s, 4s, 8s, 16s y 32s, aplicando el jitter propio de la cola. Cuando implementes el backoff manualmente, añade el jitter tú mismo y limita el retraso máximo para que un job no se quede dormido durante un día entero:
function nextDelay(attempt: number, base = 1_000, cap = 60_000) {
const exponential = Math.min(cap, base * 2 ** attempt);
const jitter = Math.random() * exponential * 0.5;
return Math.round(exponential + jitter);
}
Establece un límite de intentos y decide deliberadamente qué sucede cuando se alcanza. Algunos jobs deberían reintentar indefinidamente con una frecuencia baja —una tarea de reconciliación, por ejemplo— pero la mayoría debería detenerse y solicitar ayuda.
Colas de mensajes no entregados (Dead-letter queues) y mensajes venenosos
Un mensaje venenoso (poison message) es un trabajo que falla cada vez que se ejecuta: datos mal formados, un bug en el handler o una fila referenciada que no existe. Debido a que siempre falla, consume un slot de worker en cada intento y puede dejar sin recursos a los trabajos saludables. Reintentarlo infinitamente es peor que no reintentarlo en absoluto.
La solución es una dead-letter queue (DLQ). Una vez que se alcanza el límite de intentos, la cola mueve el trabajo —payload, intentos y último error— a un área de retención separada. Los workers nunca interactúan con ella, por lo que no puede bloquear otros procesos, y un operador puede inspeccionarla, corregir la causa y volver a ejecutarla.
const worker = new Worker("emails", handler, { connection });
worker.on("failed", async (job, err) => {
if (job && job.attemptsMade >= (job.opts.attempts ?? 1)) {
await deadLetter.add("failed-email", {
payload: job.data,
error: err.message,
failedAt: new Date().toISOString(),
});
await alerting.notify(`job ${job.id} exhausted retries`);
}
});
Trata la DLQ como una superficie operativa, no como un cementerio. Configura alertas cuando su profundidad aumente, asígnale un dashboard y construye una ruta de reintento (replay). Una DLQ que nadie lee es el lugar donde los bugs se esconden.
Concurrencia, rate limiting y backpressure
La concurrencia de un worker es la cantidad de jobs que procesa simultáneamente. Aumentarla incrementa el throughput hasta que el worker agota la CPU, las conexiones a la base de datos o la memoria, momento en el cual la situación empeora. El valor ideal es el número más alto que mantenga cada dependencia cómodamente por debajo de su límite.
La concurrencia es también la primera línea de defensa contra un thundering herd. Si una API downstream permite 50 solicitudes por segundo, un worker con una concurrencia de 200 la saturará. Muchas colas ofrecen un rate limiter precisamente para esto:
const worker = new Worker("sync", handler, {
connection,
concurrency: 10,
limiter: { max: 50, duration: 1_000 },
});
El backpressure es lo que evita que la cola crezca sin límites. Si los productores añaden jobs más rápido de lo que los workers pueden procesarlos, la cola se convierte en un backlog en crecimiento constante y su latencia pasa a ser de horas. Las opciones incluyen pausar a los productores cuando la profundidad supera un umbral, rechazar jobs de baja prioridad y escalar los workers automáticamente. Una cola que solo crece es una caída del servicio que aún no ha sido detectada.
Mantén las colas separadas según la carga de trabajo. Una importación nocturna lenta y un restablecimiento de contraseña sensible al tiempo no deberían compartir la misma línea.
Agrupación de escrituras en la base de datos
La base de datos suele ser el cuello de botella en un trabajo por lotes (batch job), y la causa más común es comunicarse con ella fila por fila. Cada viaje de ida y vuelta tiene una sobrecarga fija —red, parseo, planificación— que eclipsa el coste de la fila en sí. Un bucle de inserciones individuales pasa la mayor parte del tiempo esperando.
Una inserción de múltiples filas mueve los mismos datos en una sola sentencia:
const values = chunk
.map((_, n) => `($${n * 3 + 1}, $${n * 3 + 2}, $${n * 3 + 3})`)
.join(",");
await pool.query(
`INSERT INTO orders (user_id, status, total_cents)
VALUES ${values}
ON CONFLICT (external_id) DO UPDATE
SET status = EXCLUDED.status`,
chunk.flatMap((r) => [r.userId, r.status, r.totalCents]),
);
La cláusula ON CONFLICT ... DO UPDATE convierte la inserción en un upsert, que es lo que hace que una escritura masiva sea idempotente. Volver a ejecutar el trabajo actualiza las filas existentes en lugar de crear duplicados, por lo que un reintento es seguro.
Dos advertencias. Mantén los fragmentos (chunks) acotados —desde unos pocos cientos hasta unos pocos miles de filas— ya que una sentencia parametrizada tiene un límite de parámetros y una sentencia muy grande mantiene los bloqueos y la memoria durante más tiempo. Además, envuelve un fragmento en una transacción si las filas deben insertarse juntas, pero mantén la transacción corta para que no bloquee a otros escritores.
Para cargas muy grandes, una ruta de carga masiva dedicada como COPY de Postgres es aún más rápida, tal como se explica en la guía de PostgreSQL.
Fragmentación de conjuntos de datos grandes
Un proceso por lotes (batch job) que procesa a “todos los usuarios” no puede cargarlos todos en memoria. La solución es fragmentar (chunk) el trabajo: procesar una página delimitada, confirmar los cambios y luego obtener la siguiente. Esto mantiene el uso de memoria estable y permite que el proceso se reanude desde donde se detuvo.
La paginación por keyset es la forma más robusta de lograrlo. En lugar de OFFSET, que se vuelve más lento a medida que crece y puede omitir o repetir filas cuando los datos cambian internamente, recuerdas la última clave que viste:
let cursor: string | null = null;
while (true) {
const batch = await pool.query(
`SELECT id, email FROM users
WHERE ($1::text IS NULL OR id > $1)
ORDER BY id
LIMIT 1000`,
[cursor],
);
if (batch.rowCount === 0) break;
await processBatch(batch.rows);
cursor = batch.rows[batch.rows.length - 1].id;
}
Cada fragmento es independiente, por lo que un fallo a mitad de la ejecución solo provoca la pérdida del fragmento actual, y el proceso puede reanudarse pasando el último cursor. Esto se complementa naturalmente con una cola: encola un trabajo por cada fragmento para que un único fallo no obligue a reiniciar toda la ejecución.
Programación de tareas recurrentes
El trabajo recurrente —resúmenes diarios, limpieza, conciliación— se expresa mejor como una tarea repetible en lugar de una entrada de cron que llame a un endpoint HTTP. De este modo, la cola es la encargada de gestionar el horario, la protección contra solapamientos y la política de reintentos.
await reportsQueue.add(
"daily-digest",
{ region: "eu" },
{
repeat: { pattern: "0 7 * * *" },
jobId: "daily-digest:eu",
attempts: 3,
},
);
El jobId estable es fundamental: evita que el programador apile una nueva copia si hay una todavía en ejecución, y permite que cada instancia de la aplicación registre el mismo horario sin crear duplicados. Aun así, es recomendable utilizar un bloqueo distribuido alrededor del trabajo real para aquellas tareas que nunca deban ejecutarse dos veces de forma concurrente.
Prioriza el uso de UTC para los horarios y haz que la tarea sea consciente de las zonas horarias al formatear la salida. Un resumen que se envía a las 07:00 UTC no es lo mismo que uno a las 07:00 locales, y esa diferencia se traduce en un ticket de soporte cada vez que hay un cambio de horario de verano.
Observabilidad
Un job en segundo plano es invisible a menos que lo hagas visible. Cuatro señales cubren la mayor parte de lo que necesitas.
- Profundidad de la cola (Queue depth) — cuántos jobs están esperando. Una profundidad creciente significa que los workers no pueden mantener el ritmo, lo cual es la primera advertencia de una caída del servicio.
- Duración del job — un histograma por nombre de job. Un p95 que aumenta con el tiempo indica que una dependencia se está volviendo lenta.
- Tasa de fallos — fallos por minuto, desglosados por nombre de job. Un pico después de un deploy apunta directamente al cambio realizado.
- Antigüedad del job más antiguo en espera — la profundidad indica cuántos hay, la antigüedad indica qué tan grave es. Diez mil jobs que se procesan en un segundo están bien; diez que han esperado una hora, no.
Añade un correlation id a cada payload del job e inclúyelo en los logs, para que un job pueda rastrearse desde la solicitud que lo creó a través de cada reintento. Sin esto, depurar el fallo de un worker implica hacer grep de timestamps y adivinar.
await queue.add("import", { importId, correlationId: req.id });
Expón las métricas en el mismo dashboard que tu API, y configura alertas basadas en la profundidad y la antigüedad del job más antiguo, en lugar de alertas por fallos individuales, ya que estos últimos son esperados.
Apagado controlado (Graceful shutdown)
Un worker finalizado abruptamente en medio de una tarea deja dicha tarea en un estado ambiguo. La cola eventualmente la volverá a entregar, lo cual es correcto pero ineficiente, y un cierre forzado puede interrumpir una transacción de base de datos en el peor momento posible.
Gestiona SIGTERM y cierra el worker de manera deliberada:
process.on("SIGTERM", async () => {
await worker.close();
await connection.quit();
process.exit(0);
});
worker.close() deja de aceptar nuevas tareas y espera a que las que están en curso finalicen. Combínalo con un periodo de gracia de despliegue lo suficientemente largo para la tarea más lenta, y limita los timeouts de las tareas para que ninguna pueda superarlo. Si una tarea es genuinamente larga, implementa puntos de control (checkpoints) de progreso para que pueda reanudarse en lugar de reiniciarse.
La misma disciplina se aplica a la conexión: cierra el pool de la base de datos y el cliente del broker para que el proceso termine limpiamente en lugar de quedar colgado por sockets abiertos.
Mejores prácticas
- Mantén los request handlers limitados a una escritura y un enqueue; devuelve
202cuando el trabajo se difiera. - Haz que cada handler sea idempotente, ya que la entrega es at-least-once.
- Deriva las claves de deduplicación del trabajo en sí, no de un job id aleatorio.
- Utiliza exponential backoff con jitter y limita el tiempo de retraso.
- Establece un límite de intentos y redirige los jobs agotados a una dead-letter queue.
- Separa las queues por carga de trabajo para que los jobs lentos no bloqueen los urgentes.
- Limita la concurrencia según lo que tu base de datos y las APIs downstream puedan soportar.
- Agrupa las escrituras en la base de datos mediante multi-row inserts o upserts, en chunks limitados.
- Pagina conjuntos de datos grandes con keyset pagination, no con
OFFSET. - Monitorea la profundidad de la queue, la duración del job, la tasa de errores y la antigüedad del job más viejo.
- Apaga los workers de forma controlada (gracefully) y asigna un periodo de gracia equivalente a los despliegues.
Errores comunes
- Realizar llamadas lentas a servicios de terceros dentro de la solicitud y considerarlo correcto solo porque funciona en local.
- Asumir que un job se ejecuta exactamente una vez y terminar cobrando dos veces a un cliente.
- Reintentar inmediatamente sin un backoff, amplificando así el fallo original.
- Reintentar un poison message indefinidamente, bloqueando la cola.
- Ejecutar un único job gigante que procese cada fila en una sola transacción.
- Insertar filas una por una y culpar a la base de datos.
- Configurar una concurrencia tan alta que la base de datos alcance su límite de conexiones.
- Programar jobs recurrentes con un id aleatorio, acumulando duplicados.
- No revisar nunca la dead-letter queue.
- Matar los workers con
SIGKILLy perder el trabajo que estaba en curso. - No incluir la profundidad de la cola en el dashboard hasta que ocurre el primer incidente.
Próximos pasos
Una cola es tan buena como el almacenamiento que la respalda, por lo que la guía de Redis es la lectura natural para el broker que utilizan la mayoría de los equipos de Node.js. La guía de Caching muestra cómo evitar realizar el trabajo por completo, y la de Connection Pooling explica cómo evitar que una flota de workers agote tu base de datos. Cuando el trabajo consiste en una escritura masiva, la guía de PostgreSQL cubre COPY, upserts y las estructuras de transacciones que lo hacen rápido.