Feature Flags

Cómo implementar pruebas A/B en el servidor (paso a paso)

Cómo implementar pruebas A/B server-side: SDK, hashing determinístico, paridad entre servicios y validación A/A antes de confiar en el pipeline.

Ilustración abstracta de racks de servidores en verde oscuro y turquesa conectados por líneas de red brillantes que se dividen en dos caminos equilibrados, representando el enrutamiento determinístico

Implementar pruebas A/B server-side significa mover la decisión de qué variación mostrar desde el navegador del visitante hacia tu backend, antes de que cualquier respuesta HTML o de API salga hacia el cliente. Ya cubrimos por qué los equipos toman esa decisión, y el problema del flicker que resuelve, en nuestra guía de test A/B client-side vs server-side. Esta pieza es el “cómo”, la parte que esa guía deja afuera: el camino práctico, paso a paso, para lanzar una prueba server-side sobre la misma infraestructura de feature flags que el mercado ya ofrece, en lugar de construir un motor de experimentación desde cero.

Esto no es una introducción conceptual, es una guía de implementación. Cubrimos qué instalar, en qué parte de tu código debería vivir la decisión, cómo lograr que el mismo usuario reciba siempre la misma variación, cómo mantener a varios servicios de acuerdo, cómo probar el pipeline antes de confiar en su salida, y las trampas que con más frecuencia rompen un rollout server-side en el primer intento.

Prerrequisitos antes de escribir código

Hay tres cosas que deben decidirse antes de la primera línea de código de implementación:

Ninguno de estos tres elementos requiere construir tu propia función de hashing o un servidor de configuración desde cero: el SDK que elijas ya se encarga de la evaluación, el hashing y la entrega de configuración. El trabajo de ingeniería real es decidir dónde encajan esas llamadas y disciplinar al equipo para que no reevalúe el mismo flag de forma distinta en distintos puntos del código.

El pipeline de request a variación, paso a paso

El flujo es el mismo sin importar qué SDK elijas: el request llega, el servidor evalúa el flag usando el SDK y el atributo de hashing, la respuesta se arma con la variación ya decidida, y solo entonces se registra el evento de exposición. Ningún paso depende de que el navegador ejecute JavaScript adicional.

Pipeline de una prueba A/B server-sideEl request llega al servidor, el SDK evalúa el flag usando el atributo de hashing del usuario, la respuesta se arma con la variación ya decidida, y solo entonces se registra el evento de exposición.Requestllega al servidorEvaluación del flagSDK de servidorhash(usuario + experimento)decide la variaciónRespuestaya con la variación finalEvento deexposiciónregistrado una vez
Ningún paso depende del navegador: el visitante recibe una respuesta que ya trae la variación correcta, sin un segundo paso que reescriba nada del lado del cliente.

En código, los cuatro pasos se ven así, usando un SDK de servidor genérico como referencia (la sintaxis exacta cambia entre GrowthBook, Statsig, LaunchDarkly o Split, pero la secuencia lógica es idéntica en los cuatro):

  1. Inicializá el SDK una sola vez, al arrancar el servicio, nunca por request. El SDK de Node.js de GrowthBook, por ejemplo, descarga el payload de features por red durante init() y después responde cada llamada de evaluación localmente, sin viaje de red por request. La propia documentación del SDK de servidor de Statsig describe el mismo patrón: “después de que initialize termina, virtualmente todas las operaciones del SDK son síncronas”, con el SDK refrescando su payload en segundo plano de forma independiente a cualquier llamada de API.
  2. Armá el contexto del usuario con el atributo de hashing. Típicamente es un objeto que lleva el ID del usuario (o un ID anónimo) más atributos de targeting como país, plan o dispositivo. Este objeto es lo que el SDK usa para decidir la variación, así que tiene que existir antes de que ocurra cualquier evaluación, normalmente justo después de que el request se autentica.
  3. Evaluá el flag o experimento y usá el resultado para armar la respuesta. La llamada devuelve la variación decidida (por ejemplo "control" o "variant-b"), que tu código usa para elegir qué plantilla renderizar, qué valor de configuración aplicar, o qué rama de lógica seguir. No hay un segundo paso de “reescritura” después: la respuesta que el servidor envía ya es final.
  4. Registrá el evento de exposición, normalmente automático: la mayoría de los SDKs de servidor ya registran que un usuario dado “entró” al experimento la primera vez que el flag se evalúa para él, mediante un callback de tracking. Así es exactamente como se dispara automáticamente el trackingCallback de GrowthBook una vez que se calcula el resultado de un experimento. El detalle que vale la pena cuidar, cubierto más a fondo más adelante, es nunca disparar ese log más de una vez por usuario.

Hashing determinístico: la pieza que sostiene la asignación

La pieza sobre la que descansa todo lo demás es simple de describir y fácil de arruinar en la práctica: el mismo usuario debe ver la misma variación en cada request, en cada sesión, mientras el experimento esté vivo. Eso no se logra tirando un dado en cada llamada, se logra con hashing determinístico.

El principio, usado con pequeñas diferencias de implementación por GrowthBook, Statsig, LaunchDarkly y Split, es combinar el identificador del usuario con una clave de experimento fija (la “semilla”) y pasar el valor combinado por una función de hash. La propia documentación de GrowthBook describe exactamente este mecanismo, hasheando el ID del usuario junto con la clave del experimento para producir un número entre 0 y 1, con cada variación reclamando una porción de ese rango. Harness FME (Split) documenta el mismo principio subyacente: un hash determinístico de la clave del usuario y la semilla de tráfico del experimento, normalizado en un conjunto fijo de buckets, de modo que el mismo usuario cae de forma consistente en el mismo bucket para un experimento dado.

Dos propiedades importan acá:

Cómo un hash determinístico decide la variaciónHashear el usuario más la clave del experimento produce un número entre 0 y 1. El rango de 0 a 0.5 es la variación A, el rango de 0.5 a 1 es la variación B. Un usuario cuyo hash cae en 0.62 queda dentro del rango de la variación B y siempre recibe B mientras el experimento esté corriendo.A · Control (0 a 0.5)B · Variante (0.5 a 1)00.51hash = 0.62hash(usuario_123 + “checkout-v2”)recalculado, siempre el mismo número
El mismo usuario y la misma clave de experimento siempre producen el mismo hash, así que el cálculo es reproducible sin estado guardado, tanto en la primera visita como en la centésima.

Un detalle en el que tropiezan los equipos: el hashing determinístico mantiene la asignación estable mientras los parámetros del experimento no cambien. Si cambiás la división de tráfico a mitad del experimento (de 50/50 a 90/10, por ejemplo), el hash de cada usuario se mantiene igual, pero el límite entre rangos se mueve, y los usuarios cercanos al nuevo límite pueden cambiar de variación. Para este caso específico, GrowthBook ofrece sticky bucketing: una capa adicional que persiste la variación ya asignada (en una cookie, Redis u otro almacén) y la mantiene fija incluso cuando la configuración del experimento cambia por debajo, incluyendo soporte para un atributo de hash primario (el ID del usuario logueado) con un atributo de respaldo (un ID anónimo), de modo que un usuario mantenga la misma variación incluso después de cambiar de dispositivo tras iniciar sesión.

Dónde vive la decisión en tu stack

El hash resuelve “cuál variación”, pero no “en qué capa del código se calcula eso”. Hay tres patrones comunes, cada uno con un balance distinto entre velocidad, control y esfuerzo operativo:

Enfoque Dónde ocurre la decisión Latencia típica Cuándo tiene sentido
SDK directo en el servicio Dentro del mismo backend que ya procesa el request (el route handler, el servicio de producto) Ninguna extra: una llamada de función local, dado que el SDK ya cargó el payload en memoria Cuando la prueba afecta lógica que ya vive en ese servicio (precio, un permiso, una regla de negocio) y agregar una capa nueva solo para el flag no compensa
Edge function / middleware Una capa antes del handler principal, normalmente una edge function (Cloudflare Workers, Vercel Edge, Fastly Compute, Akamai EdgeWorkers) o un middleware de framework Baja, y más baja mientras más cerca esté el nodo edge del visitante geográficamente Cuando la variación afecta HTML renderizado en el servidor y querés decidir antes de armar la página, sin esperar la respuesta del backend de origen
Proxy dedicado Un servicio interno separado cuya única función es evaluar flags y devolver la decisión a quien la pida Agrega un salto de red interno extra, salvo que esté co-ubicado Cuando varios servicios distintos, a veces en lenguajes distintos, necesitan la misma decisión y querés centralizar la lógica de evaluación en un solo lugar en vez de replicar el SDK por lenguaje

Ninguna fila de esa tabla es universalmente “la mejor”. Statsig documenta la opción de edge (vía integraciones con Cloudflare Workers, Fastly Compute, AWS CloudFront/Lambda@Edge y Akamai EdgeWorkers) precisamente para achicar la distancia entre la decisión y el visitante cuando la prueba afecta lo que se renderiza en la primera respuesta. Cuando en cambio el flag decide una regla de producto que solo un servicio específico ejecuta (un servicio de facturación decidiendo qué tabla de precios aplicar, por ejemplo), poner el SDK directamente ahí, sin proxy, tiende a ser más simple de construir y más fácil de mantener funcionando.

Paridad entre varios servicios

Una prueba server-side rara vez vive dentro de un solo proceso. Un checkout, por ejemplo, podría tocar un servicio de catálogo, un servicio de precios y un servicio de pagos, y los tres necesitan estar de acuerdo en qué variación está viendo ese usuario, o la prueba mezcla variaciones dentro del mismo flujo y el resultado queda sin valor.

La buena noticia: como el hash es determinístico, la paridad no depende de que los servicios se comuniquen en tiempo real. Depende de que tres cosas sean idénticas en cada servicio que evalúa ese experimento:

Paridad de variación entre varios serviciosLa misma definición de experimento, con la misma clave y el mismo atributo de hashing, se distribuye a tres servicios distintos: catálogo, precios y pagos. Como el cálculo de hash es determinístico, los tres llegan a la misma variación para el mismo usuario, sin necesidad de comunicarse entre sí en tiempo real.Definición del experimentomisma clave, mismo atributo de hashServicio de catálogovariación: BServicio de preciosvariación: BServicio de pagosvariación: Blos tres calculan el mismo hash, de forma independiente,y llegan a la misma variación sin comunicarse entre sí
La paridad no es sincronización en tiempo real, es consistencia de configuración: cada servicio que lee la misma definición de experimento llega al mismo resultado por su cuenta.

Martin Fowler describe este mismo requisito en términos arquitectónicos, escribiendo que el router de un toggle de experimento toma su decisión de enrutamiento “quizás usando algún tipo de algoritmo de cohortes consistente basado en el ID de ese usuario” para garantizar que una persona experimente el mismo camino de código en requests subsiguientes, y recomendando que la lógica de decisión se concentre en un solo punto (lo que él llama un objeto FeatureDecisions) en lugar de dispersar la verificación del flag sueltamente por el código de cada servicio. En la práctica, eso significa: escribí la verificación del flag una sola vez, encapsulada, y reusá esa misma llamada en cada servicio, en lugar de dejar que cada equipo reimplemente su propia versión de la lógica de evaluación.

QA: corré una prueba A/A antes de confiar en el pipeline

Antes de correr tu primera prueba A/B real, corré una prueba A/A: ambos grupos ven exactamente lo mismo, y medís si la diferencia entre ellos se mantiene dentro de lo que el azar por sí solo explicaría. Si la prueba A/A reporta una diferencia estadísticamente significativa, el defecto está en tu asignación o en tu instrumentación, no en el producto, y ningún resultado A/B producido por ese pipeline antes de la corrección debería tomarse en serio.

Una prueba A/A necesita muestra suficiente para evitar un veredicto perezoso de “no se encontró diferencia” solo porque pasaron muy pocos visitantes por ella. Dimensioná la muestra y la duración para tu tráfico real antes de declarar el pipeline confiable: nuestra guía sobre cómo hacer una prueba A/B cubre el cálculo del tamaño de muestra en profundidad, y si querés el lado de la significancia de la propia lectura A/A, esta misma calculadora hace la prueba de dos proporciones por vos:

Calculadora de significancia estadística
Control (A)
Variación (B)
Control (A) · Tasa-
Variación (B) · Tasa-
Mejora relativa-
valor-p-
IC 95% de la diferencia-

Test z bilateral de dos proporciones. "Sin significancia" casi siempre significa que falta muestra, no que las versiones sean iguales.

Más allá de la prueba A/A, dos herramientas de QA reducen el riesgo de lanzar un pipeline roto:

Trampas comunes

Trampa Síntoma Corrección
Servicios desincronizados Un servicio muestra la variación A, otro muestra B, para el mismo usuario en la misma sesión Auditá si todos los servicios usan la misma clave de experimento, el mismo atributo de hashing y la misma versión de configuración; nunca dejes un servicio con una config vieja en caché
Caché de CDN sirviendo la variación equivocada Un usuario ve la variación de otro usuario, normalmente en respuestas cacheadas por URL sin tener en cuenta la cookie de asignación Incluí el atributo de hashing (o la variación ya decidida) en la clave de caché, o deshabilitá el caché en rutas con una decisión de experimento, según la CDN
Eventos de exposición duplicados El mismo usuario aparece contado dos o más veces en el mismo brazo del experimento, inflando artificialmente la muestra Registrá la exposición exactamente una vez por usuario/experimento, idealmente en el único punto donde se evalúa el flag, nunca en cada lugar del código que lee el resultado ya calculado
Reevaluar el flag en varios puntos del código Resultados inconsistentes dentro del mismo request, más exposiciones duplicadas como efecto secundario Evaluá el flag una vez por request, guardá el resultado en el contexto del request, y reusá ese valor guardado en cualquier otra parte del código

La fila de la caché merece atención extra porque falla en silencio: la variación se calcula correctamente en el servidor, pero si la respuesta cacheada se sirve a un segundo visitante sin tener en cuenta que la variación depende del usuario, ese segundo visitante recibe la respuesta calculada para el primero. La corrección depende de la CDN, pero el principio es siempre el mismo: el caché y la personalización por usuario no coexisten de forma segura sin una clave de caché que tenga en cuenta esa personalización.

Cómo medir sin duplicar eventos

La regla práctica, coherente con lo que Statsig documenta sobre el logging automático de exposición: cada SDK de servidor ya registra un evento de exposición por su cuenta, la primera vez que un usuario dado se evalúa para ese experimento, a través de un callback de tracking. El error que causa duplicados normalmente no está en el SDK, está en tu propio código: si llamás a la función de evaluación del flag en tres lugares distintos dentro del mismo request (un middleware, un componente de página y una llamada auxiliar de API), y cada llamada dispara el callback de tracking de nuevo, registrás tres exposiciones para un solo visitante.

La corrección es siempre la misma: evaluá el flag una vez por request (o por sesión de usuario, según tu caso), guardá el resultado, y reusá ese valor calculado en cualquier otro punto del código que necesite saber la variación, sin volver a llamar la evaluación. Cuando el SDK permite deshabilitar el log automático, Statsig ofrece esto explícitamente a través de métodos como manuallyLogGateExposure(); centralizar el disparo del evento en el único punto de decisión es la forma más segura de garantizar una exposición por usuario, no una por llamada.

Client-side vs server-side, en breve

Si llegaste hasta acá sin haber decidido primero entre pruebas client-side y server-side, la versión corta: client-side es más rápido de configurar y la opción común para marketing y cambios visuales de página, pero envía lógica de flags al navegador y carga con el riesgo de flicker. Server-side elimina el flicker por construcción y es la opción correcta por defecto para precios, permisos y cualquier lógica que nunca debería poder inspeccionarse desde las devtools de un navegador, al costo del trabajo de ingeniería que esta guía acaba de recorrer. El balance completo, incluyendo el mecanismo del flicker y las implicancias de SEO, vive en nuestra guía de test A/B client-side vs server-side.

Automatizá esto en Donnu

Todo lo que acabás de leer, un SDK de servidor, hashing determinístico, paridad entre servicios, overrides de QA y deduplicación de eventos, es infraestructura real para construir, y cuesta ingeniería incluso cuando partís de una herramienta lista. Para ser directos sobre qué es Donnu A/B hoy: somos una herramienta client-side, pensada para instalarse en minutos con un snippet liviano, sin requerir este tipo de trabajo de backend. Si tu prueba es un cambio visual de página (un título, un CTA, un layout, una oferta), ese es exactamente el caso de uso que resolvemos sin que tengas que armar nada de lo que describe esta guía.

Si esa es tu próxima prueba, comenzá una prueba gratis de 14 días y mirá el snippet en acción. Si tu caso realmente necesita la robustez server-side (precios, permisos de acceso, lógica de producto), esta guía es el mapa de qué construir, y vale la pena revisar la guía completa de feature flags y feature flags vs test A/B antes de elegir sobre qué herramienta server-side construir.

Leer también

Leer en inglés: how to implement server-side A/B testing, step by step. Leer en portugués: como implementar teste A/B server-side, passo a passo.

Referencias

Preguntas frecuentes

¿Necesito construir mi propio motor de experimentación para correr una prueba A/B server-side?
No. Las plataformas de feature flags y experimentación como GrowthBook, Statsig, LaunchDarkly y Split (ahora parte de Harness FME) ya traen SDKs de servidor para Node.js, Python, Go, Java y otros lenguajes comunes, así que el trabajo es de integración, no de construir un motor de hashing o evaluación desde cero. El esfuerzo de ingeniería real está en decidir en qué punto de tu backend ocurre la evaluación y en mantener estable la asignación del visitante en todos los servicios que leen ese flag.
¿Dónde debería ocurrir la decisión de variación: en el middleware, en una edge function o dentro del servicio de backend?
Depende de qué cambia la variación. Si afecta el HTML o el layout de una página renderizada en el servidor, evaluar en middleware o en una edge function antes de armar la página evita un segundo paso de reescritura y mantiene la decisión cerca del visitante. Si la variación es en cambio una regla de producto, como precio, un permiso o una pieza de lógica de negocio, evaluar directamente dentro del servicio de backend que ya es dueño de esa regla suele ser más simple que agregar una capa dedicada solo para el flag.
¿Cómo garantizo que dos microservicios asignen al mismo usuario la misma variación?
Asegurándote de que cada servicio que evalúa ese experimento use la misma clave de experimento, el mismo atributo de hashing (típicamente el ID del usuario) y la misma versión de la configuración del experimento. Como el hash es determinístico, la paridad no depende de que los servicios se comuniquen en tiempo real, depende de que todos lean exactamente la misma definición de experimento.
¿Qué es una prueba A/A y por qué correr una antes de confiar en el pipeline?
Una prueba A/A muestra exactamente la misma experiencia a ambos grupos y mide si la diferencia observada se mantiene dentro de lo que el azar por sí solo produciría. Si una prueba A/A reporta una diferencia estadísticamente significativa, el defecto está en tu instrumentación o en tu lógica de asignación, no en el producto, y ningún resultado A/B producido por ese pipeline debería tomarse en serio hasta encontrar y corregir el defecto.
¿Cómo evito contar el mismo evento de conversión dos veces?
Registrando el evento de exposición (el momento en que un usuario entra al experimento) por separado del evento de conversión, y disparando cada uno exactamente una vez por unidad de decisión, normalmente deduplicado por ID de evento o por una clave de usuario más experimento. La mayoría de los SDKs de servidor ya registran la exposición automáticamente en la primera evaluación; el error común es reevaluar el mismo flag en varios puntos del request y disparar ese log automático de exposición más de una vez para el mismo usuario.