Cloudflare D1
Produção
SQLite
Performance
Limites

D1 en producción: rendimiento, límites y lo que no escala por sí solo

El desarrollo D1 se siente rápido y sin fricciones. La producción revela límites específicos que requieren decisiones arquitectónicas que el inicio rápido no menciona.

D1 en producción: rendimiento, límites y lo que no escala por sí solo

La experiencia de desarrollo con D1 es realmente buena: un SQLite local que wrangler dev crea automáticamente, consultas que responden en unos pocos milisegundos, sin servidor que configurar, sin cadena de conexión que administrar. Esta fricción cero en el desarrollo tiende a crear la ilusión de que el banco se comportará de la misma manera en la producción. No lo hará. El límite de 2 GB por banco, las subsolicitudes que se suman, el costo de las líneas escritas por UPDATE y la aplicación de claves externas que requiere la suscripción manual por sesión son los cuatro límites que aparecen en producción y que el inicio rápido nunca menciona.

El techo de 2GB que nadie planea

D1 impone un límite de 2 GB por base de datos. Este número es fijo: no existe la opción de aumentarlo para un banco específico mediante la compra de capacidad adicional. El plan pago permite hasta 10 bancos D1, lo que significa un total teórico de 20 GB distribuidos en instancias separadas.

Para una aplicación CRUD sencilla con un volumen de datos modesto, 2 GB son suficientes durante años. Para aplicaciones con registros, historial de eventos, cargas de datos de usuario o tablas que crecen con el uso, el límite aparece antes de lo esperado. El problema no es llegar a los 2 GB en sí, sino el momento en que te das cuenta de que van a llegar, con los datos en producción y sin una estrategia de partición definida.

El camino más limpio para aplicaciones con un crecimiento predecible es particionar los datos por dominio desde el principio: una base de datos para datos transaccionales activos, otra para el historial, otra para los registros. Una aplicación SaaS puede dividirse por rangos de inquilinos: inquilinos de 1 a 1000 en el banco A, de 1001 a 2000 en el banco B. El trabajador decide a qué banco acceder según el ID del inquilino, sin que el usuario se dé cuenta. Esta arquitectura debe pensarse antes de que lleguen los primeros datos, porque refactorizar la partición con una base de datos de producción cercana al límite es una operación delicada.

El costo oculto de las subsolicitudes

Cada consulta ejecutada en D1 cuenta como una subsolicitud en el presupuesto del Trabajador. El límite para trabajadores es 1000 subsolicitudes por invocación. Esto parece espacioso hasta que identifica lo que hace una sola solicitud HTTP: autenticar el token (1 consulta), cargar el usuario (1 consulta), verificar permisos (1 consulta), recuperar la lista de recursos (1 consulta), etc. Diez consultas sobre un punto final son comunes.

El problema N+1 transforma rápidamente este número. Un punto final que enumera 50 pedidos y luego recupera los elementos de cada pedido individualmente ejecuta 1 + 50 = 51 consultas. Combinada con lecturas de 5 KV para caché y 3 accesos R2 para metadatos, esta solicitud única utiliza 59 subsolicitudes. Todavía dentro del límite, pero con un pequeño margen para puntos finales más complejos.

db.batch() resuelve N+1 sin cambiar la estructura de datos: agrupa varias consultas en una sola llamada y todas se ejecutan en una única subsolicitud. El resultado vuelve como una matriz con un elemento por consulta. Para el patrón de pedidos y artículos, db.batch() con las consultas construidas dinámicamente reduce 51 subsolicitudes a 2: una consulta para los pedidos, otra con IN para todos los artículos a la vez.

Amplificación de escritura: lo que realmente significa $1/millón de líneas escritas

El modelo de facturación D1 para escrituras es por línea afectada, no por operación. Un UPDATE que modifica 5.000 líneas cuesta 5.000 escrituras, independientemente de que sea una única llamada al banco. A 1 dólar por millón de líneas escritas, esta ACTUALIZACIÓN cuesta 0,005 dólares por ejecución.

Este número parece pequeño, pero las operaciones por lotes tienen un efecto acumulativo. Un trabajo diario que actualiza el estado de 100.000 registros como parte del procesamiento nocturno cuesta 0,10 dólares por ejecución, 3 dólares al mes solo por ese trabajo. Multiplicado por varios trabajos similares, el costo de las escrituras puede superar fácilmente el costo de las lecturas.

La capa gratuita tiene un límite de 100 mil líneas escritas por día. Una única operación de actualización por lotes puede consumir todo este límite. Esto significa que el nivel gratuito no es compatible con canalizaciones de procesamiento que realizan actualizaciones masivas; para cualquier carga de trabajo con escrituras masivas, el plan pago es el único camino a seguir.

Patrones que reducen los costos de escritura: solo agregar en lugar de actualizar (insertar un nuevo registro de estado en lugar de actualizar el existente), compresión periódica en lugar de actualizaciones continuas y procesamiento en lotes más grandes con menos frecuencia en lugar de actualizaciones granulares y frecuentes.

LLAVES EXTRANJERAS y PRAGMA: el truco por sesión

SQLite no aplica claves externas de forma predeterminada. Este comportamiento lo hereda D1 sin modificaciones. Si define FOREIGN KEY (user_id) REFERENCES users(id) en el esquema e inserta una fila con un user_id que no existe en la tabla users, D1 acepta INSERT sin error, a menos que haya ejecutado PRAGMA foreign_keys = ON en esa sesión.

El detalle crítico está "en esa sesión". PRAGMA no persiste entre conexiones. Cada invocación de trabajador que necesita aplicación de clave externa debe ejecutar PRAGMA como primera operación. Si su código inicializa la base de datos a través de un asistente, agregue PRAGMA allí y documente esto, porque eventualmente escapará a la atención de alguien del equipo.

El costo de no hacer esto es silencioso: acumula registros huérfanos sin ningún error en el registro. Descubrir el problema significa buscar manualmente referencias rotas y solucionarlo significa decidir si eliminar los registros no válidos o crear los registros principales que faltan. Si el volumen de datos corruptos es grande, la corrección se convierte en una delicada migración a producción.

Un asistente de inicio que siempre ejecute PRAGMA antes de cualquier otra operación es la inversión más sencilla posible contra este problema. La consulta PRAGMA foreign_keys = ON tarda menos de un milisegundo. El tiempo para depurar datos no válidos en producción es considerablemente más largo.

Lea también