wandres.dev
EL RUNTIME · workerd y Web APIs

Compatibility dates y flags

Cómo Cloudflare evoluciona el runtime sin romper Workers ya desplegados: fijar el comportamiento con compatibility_date y afinarlo con compatibility_flags. Reproducibilidad en el tiempo, no solo en el espacio.

⏱ 16 min

El runtime de Workers se actualiza constantemente, pero tú tienes Workers desplegados hace meses que no piensas volver a tocar. ¿Cómo avanza Cloudflare sin romperlos? Con un mecanismo elegante: cada Worker declara una compatibility_date, y el runtime se comporta ante él como se comportaba en esa fecha. El progreso queda tras una puerta que tú abres cuando quieres. Es, en el fondo, un sistema de feature flags versionado por fecha e incrustado en el propio motor.

🎯 Al terminar esta lección sabrás
  • Entender la tensión entre un runtime que avanza y Workers que no se redespliegan.
  • Fijar el comportamiento del runtime con compatibility_date.
  • Afinar comportamientos concretos con compatibility_flags.
  • Relacionar fechas y flags: una fecha es un paquete de flags por defecto.

El problema: un runtime vivo bajo código congelado

Node resuelve la evolución con semver: publican una versión mayor con cambios que rompen, y tú decides cuándo actualizar reinstalando. El control lo tiene el operador, que elige la versión del runtime que instala junto a su aplicación.

Cloudflare no tiene ese lujo. No hay una versión del runtime por Worker que tú instales; hay un único runtime global, actualizado continuamente, que ejecuta a la vez el Worker que desplegaste esta mañana y el que desplegaste hace dos años y nunca volviste a mirar. Multiplica eso por millones de Workers de clientes distintos, muchos sin mantenimiento activo, y tienes el problema en toda su crudeza: cualquier cambio de comportamiento es, potencialmente, una regresión masiva y silenciosa.

Si Cloudflare cambiara un comportamiento observable —como corrigió en su día la presencia del navigator global, o la forma de construir ciertos streams— rompería en silencio miles de Workers en producción cuyos autores no están mirando. La solución no puede ser “no cambiar nunca”, porque el runtime necesita corregir errores, cerrar agujeros de seguridad y adoptar estándares nuevos. Tampoco puede ser forzar a todos a redesplegar, porque muchos Workers están en producción sin nadie que los mantenga.

La salida es hacer que cada cambio que rompe sea opcional y esté anclado a una fecha. El runtime lleva dentro de sí todas las versiones de comportamiento a la vez, y decide cuál aplicar según lo que cada Worker declare. Es un condicional gigante sobre una fecha, no una bifurcación de binarios.

Conviene subrayar el matiz: solo se gestionan así los cambios que rompen. Las correcciones puras, las mejoras de rendimiento y las APIs nuevas que no alteran el comportamiento existente llegan a todos los Workers de inmediato, sin esperar a ninguna fecha. La puerta versionada es únicamente para lo que podría cambiar el resultado observable de un Worker que ya funciona.

compatibility_date: fijar el comportamiento

Cada Worker declara en su configuración una fecha de compatibilidad:

{
  "name": "mi-worker",
  "main": "src/index.ts",
  "compatibility_date": "2024-09-23"
}

Si prefieres el formato TOML, la misma declaración se escribe igual de directa, y puede acompañarse ya de los flags que verás en la siguiente sección:

name = "mi-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

Esa línea le dice al runtime: compórtate conmigo como te comportabas el 23 de septiembre de 2024. Aunque el motor se actualice cien veces después de esa fecha, los cambios que rompen quedan desactivados para este Worker. Su comportamiento queda congelado en el tiempo y, por tanto, es reproducible: el mismo código con la misma fecha produce el mismo resultado hoy, el año que viene y dentro de cinco.

Para adoptar mejoras, subes la fecha deliberadamente a una más reciente, pruebas, y despliegas. La actualización del runtime deja de ser un evento que te ocurre y pasa a ser una decisión que tomas. Cloudflare recomienda mantener una fecha reciente y avanzarla con regularidad, no dejarla anclada en el pasado para siempre: una fecha muy vieja acumula deuda y te aparta de las correcciones y de las APIs nuevas.

Hay un detalle práctico que se olvida a menudo: la fecha viaja con el proyecto, no con tu máquina. Vive en el wrangler.jsonc versionado —o en el wrangler.toml, si prefieres ese formato—, de modo que cualquiera que despliegue, tú, un compañero o CI, obtiene exactamente el mismo comportamiento del runtime. Es el mismo principio de reproducibilidad que un lockfile, aplicado no a las dependencias sino al motor que ejecuta tu código.

La fecha no es opcional. Si la omites, las herramientas te avisan y, según el contexto, aplican una por defecto o rechazan el despliegue, porque un Worker sin fecha declarada sería un Worker cuyo comportamiento podría cambiar bajo tus pies. Declararla siempre, de forma explícita, es parte de la higiene mínima de cualquier proyecto de Workers.

ℹ️
La fecha marca un suelo, no un techo

Fijar compatibility_date en una fecha antigua no te congela en un runtime viejo: sigues recibiendo las correcciones y mejoras que no rompen nada. Lo único que la fecha retiene son los cambios que alterarían tu comportamiento observable. Por eso una fecha vieja no es “más estable” en ningún sentido útil; es simplemente más deuda acumulada de cambios que tarde o temprano tendrás que adoptar de golpe.

compatibility_flags: control granular

La fecha es un instrumento grueso: activa de golpe todos los comportamientos cuyo día por defecto es esa fecha o uno anterior. Los compatibility_flags son el bisturí: activan o desactivan comportamientos concretos con independencia de la fecha.

{
  "compatibility_date": "2024-09-23",
  "compatibility_flags": ["nodejs_compat", "no_global_navigator"]
}

Cada flag suele venir en pareja —un nombre para activarlo y otro para desactivarlo— más una fecha en la que pasa a ser el valor por defecto. Ese doble mando te da dos superpoderes complementarios:

  • Adoptar un comportamiento nuevo antes de que sea el estándar, encendiéndolo con su flag.
  • Retrasar un comportamiento que aún no puedes absorber, apagándolo con su flag inverso pese a haberte pasado ya su fecha por defecto.
Flag para activar Flag para desactivar Qué controla
nodejs_compat El subconjunto de APIs de Node soportadas
global_navigator no_global_navigator El objeto navigator global
streams_enable_constructors Construir streams WHATWG con new

Un ejemplo concreto ayuda a ver el efecto. El flag global_navigator controla si existe el objeto navigator global, un comportamiento que se alinea con el del navegador:

// Con global_navigator activo (por fecha o por flag):
console.log(typeof navigator);     // "object"
console.log(navigator.userAgent);  // "Cloudflare-Workers"
// Sin ese comportamiento, navigator no existe y el acceso lanzaria.

No tienes que memorizar el catálogo entero: la documentación de Cloudflare mantiene una página con todos los flags, su descripción y su fecha por defecto. Algunos flags son experimentales y se marcan como tales mientras se estabilizan; con el tiempo, muchos acaban graduándose a comportamiento por defecto y quedan absorbidos por una fecha, momento en el que dejar de escribirlos explícitamente es lo correcto.

La existencia misma de los flags revela una intención de diseño: Cloudflare quiere que la adopción de cada cambio sea gradual y reversible. Puedes probar un comportamiento nuevo en un único Worker con su flag, comprobar que nada se rompe, y solo entonces subir la fecha global del proyecto para adoptarlo en todos. El flag es la unidad de experimentación; la fecha, la de compromiso.

En el día a día, los flags aparecen sobre todo en tres situaciones concretas:

  • Habilitar nodejs_compat porque una dependencia lo necesita, como verás en la lección 4.
  • Silenciar temporalmente un comportamiento nuevo que rompió algo, mientras adaptas el código.
  • Probar por adelantado una mejora anunciada, antes de que llegue su fecha por defecto.
⚠️
El flag gana a la fecha, siempre

Cuando una fecha y un flag entran en conflicto, manda el flag. Si tu compatibility_date es posterior al día en que un comportamiento pasa a ser por defecto, pero incluyes el flag que lo desactiva, ese comportamiento queda apagado. El flag es una orden explícita; la fecha, una política general, y lo explícito siempre gana. Por eso auditar el comportamiento real de un Worker exige leer las dos cosas juntas, nunca la fecha a solas.

La relación entre fechas y flags

El modelo mental exacto es este: una compatibility_date no es más que un atajo para “activa todos los flags cuya fecha por defecto sea igual o anterior a esta”. Fijar una fecha equivale a marcar una casilla por cada flag que ya se volvió estándar en ese momento.

Los compatibility_flags son entonces las excepciones que escribes encima de ese paquete: enciendes algo del futuro o apagas algo del presente. La fecha resuelve el caso común de un solo trazo; los flags cubren los casos de borde sin obligarte a mover la fecha entera.

De esta relación se deduce una consecuencia útil para razonar: dos configuraciones distintas pueden producir el mismo comportamiento. Una fecha reciente equivale a una fecha vieja más la lista explícita de todos los flags que se volvieron estándar entre ambas. Normalmente prefieres la fecha reciente, más limpia; pero entender la equivalencia te permite auditar exactamente qué comportamientos tiene activos un Worker cualquiera.

Y de aquí sale la práctica recomendada, que cierra el círculo: mantén una compatibility_date reciente en todo el proyecto, usa los compatibility_flags solo como excepciones puntuales y temporales, y trata cada avance de la fecha como un cambio revisable con su prueba en staging. Así el runtime avanza contigo, en pasos pequeños y deliberados, en lugar de acumular una deuda que algún día te obligue a saltar años de comportamiento de una vez.

flowchart TD
A[compatibility_date] --> B[activa los flags con fecha por defecto anterior o igual]
C[compatibility_flags] --> D[excepciones explicitas sobre ese paquete]
B --> E[comportamiento efectivo del Worker]
D --> E
💡
Sube la fecha como un cambio de código, no a ciegas

Avanzar la compatibility_date es tan revisable como cualquier otro cambio. Súbela en una rama, deja que tu suite corra sobre el workerd local con la fecha nueva, y despliega primero a un entorno de staging. Las notas de compatibilidad de Cloudflare detallan qué cambia en cada fecha; leerlas antes de saltar convierte una actualización arriesgada en una rutina aburrida, que es justo lo que quieres de la infraestructura.

Reproducibilidad en el tiempo, no solo en el espacio

Los sistemas de build luchan por la reproducibilidad en el espacio: que el mismo código compile igual en tu máquina y en CI. Las compatibility dates resuelven un problema más sutil y mucho menos discutido: la reproducibilidad en el tiempo. Un Worker desplegado en 2024 debe seguir comportándose en 2027 como lo hacía el día que lo escribiste, aunque el motor que hay debajo haya cambiado cientos de veces. Sin este mecanismo, Cloudflare estaría atrapada en un dilema imposible: o congela el runtime para siempre y renuncia a corregir errores y adoptar estándares, o avanza y rompe en silencio el código de millones de clientes que no están mirando. Las fechas disuelven el dilema porque desacoplan dos ejes que Node mantiene atados: el avance del motor y la versión del comportamiento que cada aplicación observa. Es la misma idea que las editions de Rust —el compilador avanza, pero cada crate declara contra qué edition se escribió— llevada al runtime y con granularidad diaria. Y la lección de diseño trasciende a Cloudflare: cuando no puedes forzar a tus usuarios a actualizarse, la compatibilidad tiene que ser un dato versionado que ellos controlan, no una promesa frágil que tú intentas no romper con cada despliegue.

⚔️ Ancla y afina el comportamiento
  1. Crea un wrangler.jsonc con una compatibility_date reciente y despliega un Worker mínimo.
  2. Añade nodejs_compat a compatibility_flags y comprueba que node:buffer pasa a estar disponible.
  3. Investiga un flag que tenga nombre de activación y de desactivación, y anota cuál es su fecha por defecto.
  4. Explica qué comportamiento efectivo resulta de combinar una fecha posterior al defecto de un flag con el flag que lo desactiva.