todos los sistemas operativostodo operativo

apenaloza.cl · bitácora

SCL (UTC−3) · v2026.10

~/blog/infraestructura-stateless-escalar-aplicaciones.md

Infraestructura stateless: la base para escalar aplicaciones sin dolor

Por qué diseñar aplicaciones sin estado en el servidor es clave para escalar horizontalmente, desplegar sin cortes y recuperarse de fallas, y cómo hacerlo bien sin caer en errores comunes.

autor
Alejandro Peñaloza
Publicado
Lectura
7 min
Diagrama de infraestructura sin estado: clientes web y móviles pasan por un balanceador de carga hacia varios servidores de aplicación idénticos que comparten base de datos, caché y almacenamiento de objetos externos
En este artículo
  1. ¿Qué significa "stateless"?
  2. El problema: escalar una aplicación con estado
  3. La arquitectura stateless
  4. Por qué importa: los beneficios reales
  5. Cómo llevarlo a la práctica
  6. Buen uso: errores comunes que conviene evitar
  7. ¿Cuándo no vale la pena?
  8. Checklist rápido
  9. Conclusión

Tarde o temprano toda aplicación que crece se topa con el mismo muro: un solo servidor ya no alcanza. Se agrega una segunda instancia detrás de un balanceador y, de pronto, los usuarios pierden la sesión, los archivos subidos "desaparecen" y los trabajos programados se ejecutan dos veces.

El problema casi nunca es la cantidad de servidores. Es el estado que cada servidor guarda por su cuenta. En este artículo explico qué significa una infraestructura stateless (sin estado), por qué es la base del escalamiento moderno y cómo llevarla a la práctica sin caer en los errores típicos.

¿Qué significa "stateless"?

Una aplicación es stateless cuando cualquier instancia puede atender cualquier petición, porque ninguna guarda información local que sea necesaria para la siguiente petición.

Eso no significa que la aplicación no tenga estado. Significa que el estado vive fuera del proceso: en una base de datos, en una caché distribuida, en un almacenamiento de objetos o en el propio cliente (por ejemplo, un token firmado).

Tipo de estado Stateful (en la instancia) Stateless (externalizado)
Sesiones de usuario Memoria o disco local Redis, base de datos o JWT
Archivos subidos Carpeta local del servidor Almacenamiento de objetos (S3 o compatible)
Caché Memoria del proceso Caché distribuida (Redis, Memcached)
Tareas en segundo plano Cron en cada servidor Cola de mensajes + workers
Configuración Archivos editados a mano Variables de entorno y gestor de secretos
Logs Archivos en disco Salida estándar enviada a un agregador

El problema: escalar una aplicación con estado

Imagina una aplicación que guarda la sesión del usuario en la memoria del servidor. Con una sola instancia funciona perfecto. Con dos, el balanceador reparte las peticiones y el usuario rebota entre servidores que no se conocen entre sí.

sequenceDiagram
    participant U as Usuario
    participant LB as Balanceador
    participant A as Instancia A
    participant B as Instancia B
    U->>LB: POST /login
    LB->>A: Reenvía petición
    A-->>U: OK, sesión guardada en memoria de A
    U->>LB: GET /mi-cuenta
    LB->>B: Reenvía petición
    B-->>U: 401 No autenticado
    Note over B: B no conoce la sesión creada en A

La solución "rápida" suele ser activar sticky sessions, es decir, que el balanceador mande siempre al mismo usuario a la misma instancia. Funciona, pero es un parche:

  • La carga se reparte de forma desigual.
  • Si la instancia cae, todos sus usuarios pierden la sesión.
  • Escalar hacia abajo (apagar instancias) corta sesiones activas.
  • Los despliegues se vuelven delicados porque cada instancia "importa".

La arquitectura stateless

En un diseño stateless, las instancias de la aplicación son intercambiables y desechables. Todo lo que debe persistir sale hacia servicios especializados.

flowchart TB
    U["Usuarios"] --> LB["Balanceador de carga"]
    LB --> A1["App instancia 1"]
    LB --> A2["App instancia 2"]
    LB --> A3["App instancia N"]

    subgraph Estado["Capa de estado compartido"]
        DB[("Base de datos")]
        R[("Caché / sesiones")]
        S3[("Almacenamiento de objetos")]
        Q[["Cola de mensajes"]]
    end

    A1 --> DB
    A2 --> DB
    A3 --> DB
    A1 --> R
    A2 --> R
    A3 --> R
    A1 --> S3
    A2 --> S3
    A3 --> S3
    A1 --> Q
    A2 --> Q
    A3 --> Q
    Q --> W["Workers en segundo plano"]
    W --> DB

La idea clave: la capa de aplicación escala horizontalmente sin coordinación, mientras la capa de estado se escala con herramientas pensadas para eso (réplicas de lectura, clústeres de caché, almacenamiento gestionado).

Por qué importa: los beneficios reales

1. Escalamiento horizontal inmediato

Si cualquier instancia puede atender cualquier petición, agregar capacidad es tan simple como levantar más réplicas. Esto habilita el autoescalado: el orquestador crea o destruye instancias según CPU, memoria, latencia o largo de la cola.

flowchart LR
    M["Métricas: CPU, latencia, peticiones/s"] --> AS{"¿Supera el umbral?"}
    AS -- Sí --> UP["Agregar réplicas"]
    AS -- No, carga baja --> DOWN["Quitar réplicas"]
    AS -- Dentro del rango --> KEEP["Mantener"]
    UP --> M
    DOWN --> M
    KEEP --> M

2. Resiliencia ante fallas

Cuando una instancia muere, no se pierde nada importante. El balanceador deja de enviarle tráfico y el orquestador la reemplaza. El usuario, en el peor caso, reintenta una petición.

3. Despliegues sin cortes

Con instancias desechables es posible hacer rolling updates o despliegues blue/green: se levantan instancias con la versión nueva, se verifica que estén sanas y se retiran las antiguas, sin ventana de mantención.

flowchart LR
    subgraph Antes
        V1a["v1"]
        V1b["v1"]
        V1c["v1"]
    end
    subgraph Durante
        V2a["v2"]
        V1d["v1"]
        V1e["v1"]
    end
    subgraph Despues["Después"]
        V2b["v2"]
        V2c["v2"]
        V2d["v2"]
    end
    Antes --> Durante --> Despues

4. Costos más eficientes

Pagar por capacidad fija dimensionada para el peak es caro. Con instancias stateless se puede escalar hacia abajo en horas valle sin miedo a cortar sesiones, y aprovechar instancias más baratas (como spot o preemptible) porque perder una no es un drama.

5. Entornos reproducibles

Si la instancia no tiene nada especial, se puede describir completa como código: una imagen de contenedor más variables de entorno. Eso hace que desarrollo, pruebas y producción se parezcan mucho más.

Cómo llevarlo a la práctica

Sesiones fuera del proceso

Hay dos caminos principales:

  • Sesión en un almacén compartido (Redis o base de datos): el servidor guarda solo un identificador en una cookie. Permite invalidar sesiones al instante.
  • Tokens firmados (por ejemplo, JWT): el estado viaja en el token. No requiere almacén, pero revocar es más difícil, así que conviene usar expiraciones cortas y refresh tokens.

En muchos frameworks el cambio es de configuración. Por ejemplo, en Laravel:

SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=redis
FILESYSTEM_DISK=s3

Archivos en almacenamiento de objetos

Nada de guardar subidas en /var/www/uploads. Los archivos van a un almacenamiento de objetos y la aplicación solo guarda la referencia. Para archivos grandes conviene usar URLs prefirmadas, de modo que el cliente suba directo al almacenamiento sin pasar por la aplicación.

sequenceDiagram
    participant C as Cliente
    participant API as API stateless
    participant S as Almacenamiento de objetos
    C->>API: Solicita subir archivo
    API-->>C: URL prefirmada con expiración
    C->>S: PUT archivo directo
    S-->>C: 200 OK
    C->>API: Confirma subida con la clave del objeto
    API->>API: Guarda la referencia en la base de datos

Tareas en segundo plano con colas

Un cron configurado en cada servidor se ejecuta N veces si hay N instancias. La alternativa:

  • Las peticiones encolan trabajos en una cola de mensajes.
  • Un grupo separado de workers los consume, y también escala de forma independiente.
  • Las tareas programadas se disparan desde un único planificador o usando un lock distribuido para garantizar una sola ejecución.

Configuración y secretos

Siguiendo el principio de The Twelve-Factor App, la configuración entra por variables de entorno y los secretos desde un gestor dedicado. La misma imagen de contenedor debe poder correr en cualquier ambiente cambiando solo su configuración.

Logs como flujo de eventos

La aplicación escribe a la salida estándar y la plataforma se encarga de recolectar y centralizar. Si los logs viven en el disco de una instancia efímera, se pierden con ella.

Health checks

Cada instancia debe exponer endpoints que el orquestador y el balanceador puedan consultar:

// Liveness: ¿el proceso está vivo?
app.get('/healthz', (_req, res) => res.status(200).send('ok'));

// Readiness: ¿puede atender tráfico? (depende de sus servicios externos)
app.get('/readyz', async (_req, res) => {
  const dbOk = await db.ping().catch(() => false);
  const cacheOk = await redis.ping().then(() => true).catch(() => false);
  res.status(dbOk && cacheOk ? 200 : 503).json({ db: dbOk, cache: cacheOk });
});

Apagado ordenado

Cuando el orquestador retira una instancia, envía una señal (SIGTERM). La aplicación debe dejar de aceptar peticiones nuevas, terminar las que están en curso y cerrar conexiones antes de salir.

process.on('SIGTERM', async () => {
  server.close();          // no aceptar nuevas conexiones
  await queue.pause();     // dejar de tomar trabajos
  await db.disconnect();   // cerrar recursos limpiamente
  process.exit(0);
});

Buen uso: errores comunes que conviene evitar

Pasar a stateless no es solo mover sesiones a Redis. Estos son los tropiezos más frecuentes:

  • Mover el cuello de botella a la base de datos. Si escalas la app a 20 réplicas, cada una abre su pool de conexiones. Usa un connection pooler (como PgBouncer para PostgreSQL), réplicas de lectura y caché para no saturar la base.
  • Caché local escondida. Variables estáticas, singletons con datos o cachés en memoria generan inconsistencias entre instancias. Si debe compartirse, va a la caché distribuida.
  • Escrituras en disco "temporales" que no lo son. Archivos generados que luego se descargan en otra petición fallarán cuando esa petición caiga en otra instancia.
  • No diseñar para la idempotencia. En sistemas distribuidos los reintentos son normales. Los endpoints críticos y los workers deben tolerar recibir el mismo mensaje dos veces (por ejemplo, usando claves de idempotencia).
  • WebSockets sin plan. Las conexiones persistentes son estado por naturaleza. Usa un adaptador con pub/sub (Redis, NATS) para que un mensaje llegue al cliente sin importar en qué instancia esté conectado.
  • Olvidar que la capa de estado también debe ser resiliente. Externalizar el estado traslada la responsabilidad: la base de datos, la caché y el almacenamiento necesitan respaldos, réplicas y monitoreo.
  • Migraciones que rompen versiones anteriores. Durante un rolling update conviven la versión vieja y la nueva. Los cambios de esquema deben ser compatibles hacia atrás (patrón expand and contract).
flowchart LR
    E["1. Expandir: agregar columna nueva, opcional"] --> D["2. Desplegar código que escribe en ambas"]
    D --> M["3. Migrar datos existentes"]
    M --> L["4. Desplegar código que lee solo la nueva"]
    L --> C["5. Contraer: eliminar columna antigua"]

¿Cuándo no vale la pena?

Ser honesto con el contexto también es buen uso:

  • Una herramienta interna con pocos usuarios y sin requisitos de disponibilidad puede vivir tranquila en un solo servidor.
  • Algunos sistemas son inherentemente stateful (bases de datos, motores de juego en tiempo real, sistemas de streaming) y requieren otras estrategias, como particionamiento o réplicas con consenso.

Aun así, diseñar pensando en stateless desde el inicio cuesta poco y evita una migración dolorosa cuando la aplicación crezca.

Checklist rápido

  • Las sesiones no dependen de la memoria ni del disco de la instancia.
  • Los archivos subidos van a almacenamiento de objetos.
  • La caché compartida es distribuida.
  • Las tareas pesadas pasan por una cola y workers separados.
  • Las tareas programadas se ejecutan una sola vez, aunque haya N instancias.
  • La configuración y los secretos llegan por entorno o gestor de secretos.
  • Los logs salen por la salida estándar hacia un agregador.
  • Existen health checks de liveness y readiness.
  • La aplicación maneja SIGTERM y se apaga de forma ordenada.
  • Las migraciones son compatibles con la versión anterior.
  • Puedo destruir cualquier instancia en cualquier momento sin que nadie lo note.

Ese último punto es la mejor prueba: si matar una instancia al azar te da miedo, todavía tienes estado escondido.

Conclusión

La infraestructura stateless no es una moda ni algo exclusivo de grandes empresas. Es la condición que hace posible escalar horizontalmente, desplegar sin cortes, sobrevivir a fallas y pagar solo por lo que se usa. El trabajo consiste en algo conceptualmente simple: sacar el estado de las instancias y ponerlo en servicios diseñados para cuidarlo.

Una vez hecho, la aplicación deja de ser un conjunto de servidores "mascota" que hay que cuidar uno por uno y pasa a ser un rebaño de instancias idénticas que se crean y destruyen según la demanda. Y ahí es donde escalar deja de doler.

compartir:linkedinx

Comentarios

Aún no hay comentarios. ¿Quieres dejar el primero?

Deja un comentario

Los comentarios se revisan antes de publicarse. No pido correo ni guardo tu IP.

$ ls ~/blog | grep relacionados

Sigue leyendo