Saltar al contenido
Chatbots de WhatsApp desde cero
Temario Ir al ejercicio →

Memoria con Cloudflare KV

  • ⏱ 55 minutos
  • 📶 Nivel: intermedio
  • 🧩 Incluye ejercicio

💡 🎯 Al terminar esta lección podrás…

  • Explicar por qué un Worker necesita memoria externa y qué lugar ocupa KV en la arquitectura del bot.
  • Diseñar claves con prefijo y asignar un TTL distinto a sesiones, pedidos e ids de mensajes ya vistos.
  • Leer y guardar JSON en KV con get y put usando expirationTtl, y comprobar el vencimiento con un reloj controlado.
  • Reconocer las limitaciones de KV (consistencia eventual, sin atomicidad, un list de máximo 1000 claves, cuotas del plan gratuito).
  • Decidir cuándo conviene pasar a D1 o Durable Objects.

⚠️ INFO REFERENCIAL: revisa los sitios oficiales

Los precios, planes, límites y tarifas de esta lección son solo una referencia (revisados en octubre de 2026) y cambian con frecuencia. Antes de decidir o cobrar a un cliente, confirma siempre en los sitios oficiales de cada proveedor (enlaces al final de la lección).

1.Por qué el Worker necesita memoria

En la lección 5 el bot recordaba el carrito porque la sesión vivía en una variable de tu programa. En la consola funciona: el programa sigue encendido mientras conversas. En la nube no.

Un Cloudflare Worker se parece más a una calculadora que a un empleado sentado en el mostrador: llega una petición, se ejecuta tu código, se responde y todo lo que había en variables desaparece. La siguiente petición puede ejecutarse en otro servidor, en otra ciudad y en otro momento. Si el cliente escribe «2 leches» y dos minutos después «listo», el Worker que recibe el segundo mensaje no tiene idea del primero.

Por eso el motor del curso es una función pura: procesar(sesion, entrada) devuelve { sesion, respuestas }. Tú le entregas la sesión, él te devuelve la nueva, y tú (el Worker) te encargas de guardarla en algún lugar que sobreviva entre peticiones. Ese lugar, en el código de referencia, es Cloudflare KV.

Mensajewebhook de Meta
→llega al Worker
Workerlee sesion:<tel>
→la pasa al motor
Motorprocesar()
→sesión nueva
Workerguarda en KV

2.Qué es KV y para qué sirve

KV (Key-Value, «clave-valor») es un almacén de Cloudflare que funciona como un diccionario gigante y global: guardas un texto bajo un nombre (la clave) y después lo recuperas con ese mismo nombre. No hay tablas, ni columnas, ni consultas: solo get, put, delete y list.

Se usa en el Worker a través de un binding: un nombre (en nuestro caso BOT_KV) que Cloudflare conecta a tu código. Lo declaras en wrangler.toml; así lo trae el proyecto del curso:

wrangler.toml
[[kv_namespaces]]
binding = "BOT_KV"
id = "00000000000000000000000000000000"          # id de EJEMPLO: reemplázalo
preview_id = "11111111111111111111111111111111"  # id de EJEMPLO para wrangler dev

El id real se obtiene creando el namespace (el «cajón» de claves) con npx wrangler kv namespace create BOT_KV; el comando te imprime el id que debes copiar. En el código, env.BOT_KV ya es el objeto con los cuatro métodos.

KV es ideal para datos que se leen mucho y se escriben poco, y que toleran un pequeño retraso en verse en todo el mundo. Para el bot de un minimarket encaja bien: una sesión por cliente, pedidos que se guardan una vez, y poco más. Más abajo verás para qué NO sirve.

📌 Nota · Las pruebas del curso usan un KV falso

Las pruebas de codigo/test/worker.test.js no usan internet: reemplazan BOT_KV por un objeto con un Map que imita get, put y list. Eso permite probar todo el Worker en tu computadora. En esta lección haremos lo mismo pero agregando un reloj, para ver el vencimiento.

3.Claves con prefijo: ordenar el cajón

Todo lo que guardas vive en el mismo espacio de nombres, así que conviene que cada clave diga qué tipo de dato es. La convención es un prefijo seguido de dos puntos. El Worker del curso usa tres:

Clave Contenido Vive
sesion:<telefono> La sesión completa del cliente (estado, carrito, entrega…) 2 días
pedido:<id> Un pedido confirmado 30 días
visto:<wamid> Marca de «ya procesé este mensaje» 1 día

Así es el código real que lee y guarda JSON (está en src/worker.js):

worker.js
const SEGUNDOS_DIA = 24 * 60 * 60;
const TTL_SESION = 2 * SEGUNDOS_DIA; // la sesión vive 2 días sin actividad
const TTL_PEDIDO = 30 * SEGUNDOS_DIA;
const TTL_VISTO = SEGUNDOS_DIA; // ids de mensajes ya procesados

async function leerJson(kv, clave) {
  const texto = await kv.get(clave);
  return texto ? JSON.parse(texto) : null;
}

const guardarJson = (kv, clave, valor, ttl) => kv.put(clave, JSON.stringify(valor), { expirationTtl: ttl });

Tres detalles que conviene entender:

El teléfono entra en la clave, así que cada cliente tiene su propio cajón: sesion:51999000111 nunca se mezcla con sesion:51999000222. Si el número llegara con espacios o signos, conviene normalizarlo (dejar solo dígitos) antes de armar la clave. Lo practicarás en el ejercicio.

4.El ciclo de vida de una sesión

Cada vez que llega un mensaje, procesarMensaje hace siempre lo mismo: leer, procesar, guardar. Este es el fragmento central del código real:

worker.js
const claveSesion = `sesion:${evento.de}`;
const sesionPrevia = (await leerJson(kv, claveSesion)) ?? crearSesion(evento.de, ahora);
const { sesion, respuestas } = procesar(sesionPrevia, evento.entrada, ahora);

await guardarJson(kv, claveSesion, sesion, TTL_SESION);
if (sesion.pedidoNuevo) await guardarJson(kv, `pedido:${sesion.pedidoNuevo.id}`, sesion.pedidoNuevo, TTL_PEDIDO);

Observa el ??: si no hay sesión previa (cliente nuevo, o la anterior venció), se crea una limpia. Y observa que cada escritura renueva el TTL: la sesión no vence «dos días después de crearse», sino «dos días después del último mensaje». Un cliente activo nunca pierde el carrito; uno que desaparece dos días, sí.

Lo comprobamos con un script que ejecuté contra el procesarMensaje real, usando un KV falso con reloj y vencimiento (el KV de verdad expira a su manera, pero la lógica es la misma). Después del primer mensaje, las claves y su vida restante son:

Salida esperada
visto:wamid.1            vence en 24 h
sesion:51999000111       vence en 48 h
ELIGIENDO [ '2 x Leche entera 1 L' ]

Ahora adelantamos el reloj un día, luego dos días más, y el cliente vuelve a escribir «hola»:

Salida esperada
--- 1 dia despues
visto vivo? null | sesion viva? true
--- 3 dias despues
sesion viva? false
MENU carrito: 0

Al día siguiente, la marca visto: ya se borró pero la sesión sigue viva. A los tres días, la sesión venció: el cliente empieza de cero (estado MENU, carrito vacío). Es justo lo que queremos: un carrito de hace tres días probablemente ya no importa, y mantenerlo para siempre llenaría el almacenamiento de datos viejos (y de teléfonos de personas que no volverán).

⚠️ Importante · El TTL mínimo es 60 segundos

expirationTtl no admite menos de 60 segundos: Cloudflare no soporta vencimientos que caigan a menos de un minuto en el futuro. No uses KV para bloqueos de 5 o 10 segundos. Fuente: documentación de put(). Verifica este dato cuando lo uses, porque los límites de las plataformas cambian.

Elegir un TTL con criterio

No hay un número mágico. Pregúntate cuánto tiempo tiene sentido recordar cada cosa:

Fíjate también en qué no se guarda: tokens, contraseñas ni datos de pago. La sesión contiene teléfono y dirección de entrega, que son datos personales; guardarlos por menos tiempo es una buena práctica de privacidad que retomaremos en la lección 24.

5.Idempotencia: Meta puede repetir el mismo mensaje

Meta reintenta la entrega del webhook si no recibe un 200 a tiempo, así que el mismo mensaje puede llegar dos veces. Si no haces nada, el cliente que pidió «2 leches» recibiría 4. La solución se llama idempotencia: procesar el mismo mensaje una o diez veces debe dejar el mismo resultado.

El Worker lo resuelve guardando el id único del mensaje (wamid...) en una clave visto::

worker.js
const claveVisto = `visto:${evento.id}`;
if (await kv.get(claveVisto)) return;
await kv.put(claveVisto, '1', { expirationTtl: TTL_VISTO });

La primera vez no existe y el mensaje se procesa. La segunda, la clave existe y el Worker retorna sin hacer nada. El valor da igual ('1'): lo que importa es que la clave exista. Y como tiene TTL de un día, la lista de ids no crece sin fin. Si el envío a Meta falla, el Worker restaura la sesión anterior y borra la marca visto: para que el reintento de Meta se procese de nuevo (lo verás en la lección 10).

6.Qué le cuesta a tu cuenta cada conversación

KV cobra (o limita, en el plan gratuito) por operación. Para saber cuánto gasta tu bot, lo mejor es medirlo: contamos las llamadas a un KV falso mientras un cliente completa un pedido de cinco mensajes (2 leches, listo, recojo, efectivo, confirmar):

Salida esperada
pedido completo (5 mensajes): { get: 10, put: 11, list: 0 }

Cada mensaje hace 2 lecturas (visto: y sesion:) y 2 escrituras (las mismas dos claves), y el mensaje final suma una escritura más para el pedido. Total: 11 escrituras por pedido completo.

Ahora mira los límites oficiales del plan gratuito: 100 000 lecturas por día y 1 000 escrituras por día. Con 11 escrituras por pedido, el tope teórico es de unos 90 pedidos completos al día; en la práctica, menos, porque hay clientes que solo preguntan el horario. Para un minimarket pequeño alcanza para la demo y los primeros clientes; para un negocio con mucho movimiento hay que pasar al plan de pago.

⚠️ Importante · Verifica este dato

Las cuotas de Cloudflare cambian. Antes de prometerle algo a un cliente, revisa la página de límites de KV y la de precios.

También hay una pista de optimización a la vista: guardar la marca visto: antes de procesar el mensaje cuesta una escritura por mensaje. Es el precio de la seguridad frente a duplicados; si algún día sube la factura, puedes tomar esa decisión con datos, no con intuición.

7.Las limitaciones de KV (las importantes)

KV no es una base de datos clásica. Estas son las limitaciones que debes tener presentes, todas documentadas por Cloudflare:

1. Consistencia eventual

Un cambio es visible de inmediato en el centro de datos donde se hizo, pero en otros puede tardar hasta 60 segundos o más, porque cada lugar guarda una copia en caché. Incluso las claves que no existen se cachean: una clave recién creada puede tardar en aparecer. Fuente: cómo funciona KV.

Para un cliente que conversa con calma, casi nunca se nota. El problema aparecería con dos mensajes casi simultáneos atendidos por distintos centros de datos.

2. No hay operaciones atómicas

Nuestro Worker hace «leer, modificar, escribir». Si dos mensajes del mismo cliente llegan a la vez, ambos leen la misma sesión, ambos la modifican y el último en escribir pisa el cambio del otro. Lo reproduje con un KV falso que tarda 10 ms en responder, mandando «2 leches» y «1 arroz» a la vez:

carrera.mjs
await Promise.all([
  procesarMensaje(env, msg('wamid.A', '2 leches'), ahora, llamar),
  procesarMensaje(env, msg('wamid.B', '1 arroz'), ahora, llamar),
]);
const s = JSON.parse(datos.get('sesion:51999000111'));
console.log(s.carrito.map((l) => `${l.cantidad} x ${l.nombre}`));
Salida esperada
[ '1 x Arroz extra 1 kg' ]

Las leches se perdieron. Es el clásico lost update («actualización perdida»). En la vida real los clientes rara vez escriben dos mensajes en el mismo instante, y WhatsApp tiende a entregarlos en orden, pero sucede. Si tu bot tiene mucha concurrencia, la documentación de Cloudflare recomienda Durable Objects para esos casos (un único «portero» por cliente que atiende los mensajes de uno en uno).

Fíjate que la protección visto: tampoco es atómica: también es «leer y luego escribir». Con ids distintos no hay problema, pero dos entregas simultáneas del mismo id podrían colarse. Con la frecuencia de reintentos de Meta, es un riesgo muy bajo para una demo y real para un volumen alto.

3. Un mismo dato no se puede escribir más de una vez por segundo

El límite oficial es 1 escritura por segundo a la misma clave. Para sesiones de clientes está bien. No guardes un contador global de visitas en una sola clave.

4. list devuelve un máximo de 1000 claves por llamada

El cron de recordatorios (lección 18) necesita recorrer todas las sesiones. Este es el código real de listarJson:

worker.js
async function listarJson(kv, prefijo) {
  const { keys } = await kv.list({ prefix: prefijo });
  const valores = await Promise.all(keys.map((k) => leerJson(kv, k.name)));
  return valores.filter(Boolean);
}

El comentario del propio archivo lo admite: «basta para una demo (hasta 1000 claves)». Según la documentación de list, si hay más claves la respuesta trae list_complete: false y un cursor para pedir la siguiente página; este código ignora el cursor. Con 1 500 sesiones activas, 500 quedarían sin recordatorio. Además, cada pasada del cron hace una lectura por sesión: 96 pasadas al día (cada 15 minutos) por 200 sesiones son 19 200 lecturas diarias, una quinta parte del tope gratuito, solo en recordatorios.

La salida honesta es: KV está bien para empezar, pero cuando el bot crezca, un recorrido completo por todas las sesiones es señal de que necesitas una base de datos.

5. El tamaño no es el problema

Un valor puede pesar hasta 25 MiB y una clave hasta 512 bytes. Nuestra sesión de ejemplo pesa unos 395 bytes. Si algún día guardas conversaciones enteras en una sesión, el límite de tamaño será lo último que te preocupe: el costo en escrituras lo notarás antes.

8.Cuándo pasar a otra cosa

Necesidad Herramienta
Sesiones, pedidos, flags: leer mucho, escribir poco KV (lo que usas ahora)
Consultas («pedidos de hoy», «clientes de Surco»), conteos, reportes D1 (base SQL de Cloudflare)
Un solo escritor por cliente, sin pisarse mensajes Durable Objects
El dueño quiere ver y editar los datos sin programar Google Sheets (próxima lección)

No hace falta migrar hoy. Lo importante es que el código esté ordenado para poder migrar: por eso leerJson, guardarJson y listarJson son las únicas funciones que tocan el KV. Si mañana usas D1, cambias esas tres funciones y el motor ni se entera.

9.Probar sin gastar un sol

Para mirar las claves de tu KV local mientras corre npx wrangler dev (los comandos aceptan --local para usar el almacenamiento de desarrollo y --remote para el real):

Terminal
npx wrangler kv key list --binding BOT_KV --local
npx wrangler kv key get "sesion:51999000111" --binding BOT_KV --local

Si algún comando no coincide con tu versión de Wrangler, ejecuta npx wrangler kv key --help: la sintaxis de la herramienta evoluciona y la referencia oficial es la lista de comandos de KV.

10.Errores frecuentes

11.Apuntes para llevar

12.Glosario

Término Significado
KV Almacén clave-valor global de Cloudflare, rápido para leer y con vencimiento automático.
Binding Nombre (BOT_KV) con el que Cloudflare conecta un recurso a tu Worker.
Namespace «Cajón» de KV que agrupa un conjunto de claves.
TTL Time to live: segundos que vive un dato antes de borrarse solo.
Consistencia eventual Un cambio tarda un tiempo en verse en todos los centros de datos.
Idempotencia Procesar lo mismo varias veces deja el mismo resultado que procesarlo una vez.
Durable Objects Servicio de Cloudflare con un solo «dueño» por dato, útil cuando se exige orden y atomicidad.
D1 Base de datos SQL de Cloudflare, adecuada para consultas y reportes.

🧠 Comprueba lo aprendido

Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.

Un cliente escribió «2 leches» y dos minutos después «listo». ¿Por qué el Worker necesita KV para entender el segundo mensaje?

¿Qué hace expirationTtl: 172800 al guardar una sesión con kv.put?

¿Para qué sirve la clave visto:<wamid>?

Dos mensajes del mismo cliente llegan a la vez y el carrito pierde un producto. ¿Qué limitación de KV lo explica?

El cron recorre list({ prefix: 'sesion:' }) sin cursor y tienes 1 500 sesiones. ¿Qué pasa?

Un cliente necesita saber cuántos pedidos hubo hoy por zona. ¿Qué herramienta encaja mejor?

📚 Para profundizar (fuentes oficiales)

Este material lo hace un estudiante con ayuda de IA: ante cualquier duda, manda la documentación oficial.

Tu progreso se guarda en este navegador y se refleja en el índice del curso.

💡 🚀 ¡Ahora te toca!

La práctica fija lo aprendido. Pasa al ejercicio de esta lección.

Ir al ejercicio