La propuesta de valor de Workers se vuelve clara cuando ve lo que es posible en un solo controlador: obtener un registro de D1 con SQL, verificar una entrada en KV, obtener un objeto de R2, llamar a un servicio interno a través de Service Binding y poner en cola el trabajo asincrónico en una cola, todo dentro de la misma invocación, con cada enlace inyectado como una propiedad de env y disponible con una línea de código. No hay un SDK separado para crear instancias, ni una configuración de red que administrar, ni credenciales flotando en las variables de entorno. wrangler.toml declara los enlaces, el tiempo de ejecución los entrega. El problema es que cada una de estas operaciones consume una subsolicitud del presupuesto de invocación y la factura aparece antes de lo que parece.
El modelo de vinculaciones y lo que resuelve.
Los enlaces son la forma en que el tiempo de ejecución de Workers conecta su código con las capacidades de la plataforma sin exponer las credenciales o la configuración de la red. Cuando declaras [[d1_databases]] en wrangler.toml con un database_id, el tiempo de ejecución inyecta un objeto con la interfaz D1 en env.DB. Cuando declaras [[kv_namespaces]] con un id, el tiempo de ejecución inyecta la interfaz KV en env.CACHE. No hay token de autenticación, no hay una URL de punto final, no hay un SDK de terceros; el enlace es una llamada directa a la infraestructura de Cloudflare.
Esto tiene consecuencias para la seguridad y el funcionamiento. Un trabajador comprometido no tiene credenciales para exfiltrar; solo puede operar los enlaces que le han sido declarados, con los permisos que esos enlaces tienen. Para un trabajador que solo necesita leer del KV, declara el enlace como de solo lectura y el trabajador literalmente no puede escribir, independientemente de lo que intente hacer el código. Esta separación de capacidades es más fuerte que un código de verificación de permisos.
Para la preparación y la producción, cada entorno tiene sus propios ID de enlace en wrangler.toml. El código no cambia (env.DB sigue siendo env.DB), pero durante la preparación apunta a un banco D1 diferente, un espacio de nombres KV diferente, un depósito R2 diferente. No hay riesgo de que el código provisional toque los datos de producción porque los enlaces están físicamente separados.
El presupuesto de la subsolicitud y cómo se consume
El plan gratuito tiene 50 subsolicitudes por invocación. El plan pago tiene 1000. Cada operación que deja aislado cuenta: fetch() para cualquier URL, env.KV.get(), env.KV.put(), env.DB.prepare().run(), env.BUCKET.get(), env.QUEUE.send(), env.SERVICE.fetch(). Una llamada al env.DB.batch() con 5 consultas cuenta como 1 subsolicitud; este detalle es fundamental.
Un controlador típico de una API que devuelve un perfil de usuario enriquecido: busca el usuario en D1 por ID (1), busca preferencias en KV (2), busca una foto de perfil en R2 (3), llama a un servicio de autorización a través de enlace de servicio (4). Total: 4 subsolicitudes en la ruta feliz. Se escala a 1000 usuarios simultáneos y todavía está dentro del límite: 4 subsolicitudes por invocación no son un problema.
El problema surge con los bucles. Un controlador que procesa una lista de elementos y realiza una consulta D1 por elemento (el clásico N+1) explota rápidamente. Cincuenta artículos con una consulta cada uno alcanzan el límite del plan gratuito en la primera llamada. Con 200 elementos en el plan pago, todavía está dentro de 1000, pero la latencia acumulada de 200 consultas D1 secuenciales será de cientos de milisegundos. El error cuando se alcanza el límite llega como un error de red en la subsolicitud que se excedió: no hay un mensaje claro en el cuerpo de la respuesta al cliente, solo una excepción al final.
Optimizaciones concretas mediante vinculación
Para D1, db.batch() es la optimización más importante. En lugar de hacer prepare().run() en secuencia para múltiples consultas, pasa una serie de declaraciones a batch() y recibe una serie de resultados, a un costo de 1 subsolicitud total. Para consultas que no dependen unas de otras (búsqueda de configuraciones de diferentes tipos, por ejemplo), el procesamiento por lotes elimina la latencia secuencial y el gasto de subsolicitudes al mismo tiempo.
Las consultas D1 paralelas (Promise.all([db.query1, db.query2])) todavía cuentan como subsolicitudes separadas, pero se ejecutan en paralelo y la latencia está determinada por la más lenta, no por la suma. Utilice Promise.all() cuando los resultados sean independientes y no necesite el lote por otro motivo. Utilice batch() cuando desee consolidar el costo de la subsolicitud.
Para KV, el patrón más valioso es el caché del módulo. KV tiene latencia de red (unos pocos milisegundos, incluso en el mejor de los casos) y rara vez se cambian los datos de configuración, no es necesario volver a leerlos con cada invocación. Un módulo puede declarar una variable en alcance global:
let cachedConfig = null; export default { async fetch(request, env) { if (!cachedConfig) { cachedConfig = JSON.parse(await env.CONFIG.get('app-config')); } // usa cachedConfig } }
Mientras el aislamiento viva en el PoP, las invocaciones posteriores reutilizan el valor sin desperdiciar una subsolicitud. El riesgo es la obsolescencia: si la configuración cambia, el aislado almacenado en caché seguirá usando la versión anterior hasta que se descarte. Para datos que deben ser casi en tiempo real, es más apropiado KV con cacheTtl bajo o relectura de invocación. Para indicadores de funciones y configuraciones de infraestructura que rara vez cambian, el caché del módulo es eficiente.
Para R2, el enlace admite solicitudes de rango (env.BUCKET.get(key, { range: { offset, length } })), lo que le permite recuperar solo la parte de un objeto que necesita en lugar del archivo completo. Para archivos grandes donde necesita un encabezado, metadatos incrustados o las primeras líneas de un CSV, la solicitud de rango ahorra memoria y tiempo de transferencia. La respuesta es un ReadableStream que se puede canalizar directamente al cliente sin almacenamiento en búfer.
Composición de múltiples enlaces en la misma solicitud
El verdadero poder surge cuando se necesitan múltiples tipos de datos para construir una respuesta. Una API de producto que devuelve una página de búsqueda: consulta SQL en D1 para ID que coincidan con el filtro, lectura paralela en KV para precios (que cambian con frecuencia y viven en KV para rendimiento de lectura) y URL prefirmada de R2 para la imagen principal de cada producto.
Esta composición funciona de forma natural: env.DB, env.PRICES, env.ASSETS están disponibles en el mismo controlador. El diseño que no funciona bien es hacerlo en secuencia para cada elemento de una lista. La versión correcta: la consulta D1 devuelve 20 ID, Promise.all() para las lecturas de 20 KV en paralelo (20 subsolicitudes, pero en paralelo), Promise.all() para las 20 URL R2 (20 subsolicitudes más). Total: 41 subsolicitudes (1 D1 + 20 KV + 20 R2), dentro del límite pagado de 1000, con latencia determinada por la más lenta de las 40 recuperaciones paralelas, no por la suma de 41.
Monitorear el uso de subsolicitudes
El tiempo de ejecución no expone los recuentos de subsolicitudes por invocación directamente en la cola. El enfoque es instrumentar: crear un contenedor simple que incremente un contador con cada operación de enlace y registre el total al final del controlador. Exportado a través de Logpush, este número le permite detectar cuándo una implementación ha aumentado el consumo, antes de llegar a un caso límite que supere el límite en producción.
Un controlador que utiliza 500 subsolicitudes cuando podría usar 50 con el procesamiento por lotes y el paralelismo correctos está desperdiciando latencia y presupuesto innecesariamente. Rediseñar este manipulador tras una incidencia en producción es más costoso que medir el consumo desde el principio y corregirlo mientras N aún es pequeño.
Lea también
- KV vs R2 vs Cache API: cuándo usar cada nivel de almacenamiento de Cloudflare
- Trabajadores de Cloudflare en producción: qué cambia después de hello world
- Trabajadores: depuración, registros y Workers Tail: observabilidad en el borde sin un servidor de registros
- Trabajadores: límites de CPU y memoria, que la documentación no explica bien
- Testing Workers: unidad, integración y cómo simular el tiempo de ejecución sin depender de Cloudflare
- Cloudflare KV: ¿Qué significa distribuido globalmente cuando necesitas escribir?
