🏗️ Arquitectura & Sistemas ☁️ Cloud, DevOps & Infraestructura

Escalabilidad de sistemas: de 100 a 1 millón de usuarios

Cada orden de magnitud de usuarios rompe algo distinto: primero la CPU de un servidor, luego la base de datos, luego las escrituras. Guía honesta de qué añadir en cada etapa y por qué diseñar para 1M con 100 usuarios sale caro.

📅 21 de julio de 2026 ⏱️ 10 min de lectura ✍️ Equipo ProgramacionWebs

“Diseña para escalar” es un consejo que suena responsable y que, aplicado sin criterio, produce sistemas peores. Añadir sharding de base de datos, colas de mensajes y una capa de caché distribuida a un producto con cien usuarios activos no es “prepararse para el futuro”: es gastar semanas de ingeniería en resolver un problema que no existe todavía, a costa de retrasar el problema real, que es conseguir que esos cien usuarios se conviertan en mil.

La escalabilidad no es una propiedad binaria que un sistema tiene o no tiene. Es una serie de cuellos de botella distintos que aparecen en orden, cada uno en un orden de magnitud distinto de usuarios, y cada uno con una solución específica que sería prematura en la etapa anterior. Este artículo recorre esa progresión de forma honesta: qué se rompe primero, qué se añade para resolverlo, y por qué añadirlo antes de tiempo tiene un coste real.

Etapa 1: cientos de usuarios — un servidor basta, y sobra

Con un puñado de cientos de usuarios activos, un único servidor ejecutando la aplicación y la base de datos —incluso en la misma máquina— es perfectamente viable. El cuello de botella en esta etapa casi nunca es técnico: es de producto. La energía de ingeniería que se gasta en preparar infraestructura para una escala que no existe es energía que no se gasta en averiguar si el producto le resuelve algo real a esos primeros usuarios.

Lo único que sí merece la pena en esta etapa: separar la aplicación de la base de datos en procesos distintos (aunque sigan en la misma máquina o en máquinas contiguas), y tener backups automáticos desde el primer día. Ninguna de las dos cosas es “escalar”; son higiene operativa básica.

Etapa 2: miles de usuarios — balanceo de carga y la primera réplica de lectura

Cuando el tráfico empieza a acercarse al límite de CPU o memoria de un único servidor, la primera palanca —y la más barata— es el escalado vertical: una máquina más grande. Es una solución honesta y subestimada: no añade complejidad arquitectónica, solo dinero, y en esta etapa suele ser la opción con mejor relación coste-beneficio.

Cuando el escalado vertical deja de bastar, o cuando la disponibilidad importa más que el ahorro (un único servidor es un único punto de fallo), entra el escalado horizontal: varias instancias de la aplicación detrás de un balanceador de carga.

graph TD
U[Usuarios] --> LB[Balanceador de carga]
LB --> A1[Instancia app 1]
LB --> A2[Instancia app 2]
A1 --> DB[(Base de datos primaria)]
A2 --> DB
DB -.->|replicación asíncrona| R1[(Réplica de lectura)]
A1 -.->|consultas de solo lectura| R1
A2 -.->|consultas de solo lectura| R1

En esta misma etapa suele aparecer el primer cuello de botella de base de datos: la mayoría de aplicaciones son mucho más intensivas en lecturas que en escrituras (listar productos, mostrar perfiles, cargar dashboards), así que añadir una o más réplicas de lectura —copias de la base de datos que reciben los cambios de forma asíncrona desde la primaria y solo atienden consultas de lectura— multiplica la capacidad de lectura sin tocar el modelo de datos. La documentación de AWS sobre RDS es explícita sobre el caso de uso: las réplicas de lectura sirven para escalar cargas de trabajo intensivas en lectura y para descargar consultas de generación de informes de la instancia de producción, sin que compitan por recursos con el tráfico transaccional.

La réplica no es gratis: introduce replicación asíncrona, lo que significa que una lectura contra la réplica puede no reflejar todavía una escritura hecha hace apenas unos milisegundos. Para la mayoría de pantallas (listados, catálogos, dashboards) ese margen es irrelevante; para pantallas donde el usuario espera ver inmediatamente el resultado de su propia escritura (confirmar un pago y ver el estado actualizado en la misma petición), hay que enviar esa lectura concreta a la base primaria de forma explícita, no a la réplica.

Etapa 3: decenas de miles — el caché deja de ser opcional

En algún punto entre decenas de miles de usuarios activos, la base de datos —incluso con réplicas— empieza a repetir el mismo trabajo una y otra vez: la misma consulta de “productos más vendidos” ejecutándose miles de veces por minuto cuando el resultado apenas cambia cada pocos minutos. Aquí entra el caché, normalmente con Redis o Memcached como capa intermedia entre la aplicación y la base de datos.

El patrón más extendido, y con razón, es cache-aside (también llamado lazy loading): la aplicación consulta primero el caché; si el dato está (cache hit), lo devuelve directamente sin tocar la base de datos; si no está (cache miss), lo lee de la base de datos, lo guarda en el caché y lo devuelve. Es el patrón de caché más usado para consultas de base de datos precisamente porque degrada con elegancia: si el caché entero se cae, el sistema simplemente vuelve a golpear la base de datos en cada petición, más lento pero sin caerse.

async function obtenerProductosDestacados(): Promise<Producto[]> {
  const clave = 'productos:destacados';
  const enCache = await redis.get(clave);
  if (enCache) return JSON.parse(enCache); // cache hit

  const productos = await db.query('SELECT * FROM productos WHERE destacado = true');
  await redis.set(clave, JSON.stringify(productos), { EX: 300 }); // TTL: 5 minutos
  return productos; // cache miss
}

El TTL (tiempo de vida) de cada clave es la decisión de diseño real de esta etapa, no la existencia del caché en sí: un TTL demasiado largo sirve datos obsoletos; uno demasiado corto no reduce apenas carga de base de datos. La regla práctica es fijar el TTL según cuánto tiempo el negocio tolera ver un dato ligeramente viejo, no según una cifra arbitraria.

Para datos con requisitos de consistencia más estrictos —donde una lectura obsoleta después de escribir sería un bug visible, no un detalle menor— conviene el patrón write-through, donde cada escritura actualiza el caché y la base de datos a la vez, sacrificando algo de velocidad de escritura a cambio de que el caché nunca esté desincronizado.

Etapa 4: cientos de miles — CDN, y el problema deja de ser solo el servidor

Cuando el tráfico crece más allá de una única región geográfica, la latencia de red entre el usuario y el servidor empieza a pesar tanto como cualquier optimización de base de datos. Un usuario en Buenos Aires pidiendo contenido a un servidor en Virginia sufre cientos de milisegundos de ida y vuelta antes de que el servidor procese nada. Aquí es donde una CDN (red de distribución de contenido) deja de ser un extra de rendimiento y pasa a ser estructural: sirve contenido estático —imágenes, CSS, JavaScript, y con las configuraciones adecuadas, también respuestas de API cacheables— desde un nodo geográficamente cercano al usuario, en lugar de desde el servidor de origen.

La documentación de Cloudflare sobre su capa de caché en el edge lo resume bien: combinando ubicaciones de borde por todo el mundo con una estrategia de caché correcta, es posible servir más del 90% de las peticiones directamente desde el caché de borde, con latencias de un solo dígito de milisegundos, sin que esas peticiones lleguen nunca al servidor de origen.

Etapa 5: el millón de usuarios — cuando las réplicas de lectura ya no bastan

Las réplicas de lectura resuelven el problema de escalar lecturas, pero no resuelven el de escalar escrituras: todas siguen pasando, sin excepción, por la base de datos primaria. Cuando el volumen de escrituras satura la capacidad de esa única instancia —ya sea por CPU, por IOPS de disco o simplemente porque el dataset completo ya no cabe cómodamente en la memoria disponible de un solo servidor—, la solución estructural es el sharding: partir horizontalmente los datos entre varias bases de datos independientes, cada una responsable de un subconjunto definido por una clave de partición (normalmente el ID de usuario o de tenant).

graph TD
APP[Aplicación] --> ROUTER[Capa de enrutado por shard key]
ROUTER -->|user_id % 4 == 0| S0[(Shard 0)]
ROUTER -->|user_id % 4 == 1| S1[(Shard 1)]
ROUTER -->|user_id % 4 == 2| S2[(Shard 2)]
ROUTER -->|user_id % 4 == 3| S3[(Shard 3)]

El sharding es, con diferencia, el paso más caro de toda esta progresión: reparte los datos entre instancias completamente independientes, lo que significa que una consulta que necesite combinar información de dos usuarios en shards distintos ya no puede resolverse con un simple JOIN. Herramientas como Vitess (la base de PlanetScale) existen precisamente para absorber parte de esa complejidad y presentar los shards ante la aplicación como si fueran una única base de datos, pero incluso con esas herramientas, diseñar bien la clave de partición —de forma que los datos que se consultan juntos vivan en el mismo shard— es una decisión de arquitectura que, si se hace mal, es dolorosa de corregir después con datos ya en producción.

La tabla que resume qué añadir, y cuándo no

Escala aproximadaCuello de botella típicoQué añadirQué NO añadir todavía
Cientos de usuariosNinguno técnico realBackups, separar app y BD en procesos distintosCaché, colas, sharding, microservicios
MilesCPU de un servidor, lecturas repetidasBalanceo de carga, réplicas de lecturaSharding, CDN global
Decenas de milesConsultas repetidas a la BDCaché (cache-aside con Redis)Sharding
Cientos de milesLatencia geográficaCDN para contenido estático y cacheableReescribir todo a microservicios
~1 millónVolumen de escritura en una sola instanciaSharding de la base de datos, colas para desacoplar picos de escrituraSharding especulativo sin cuello de botella medido

Recomendación práctica

La disciplina que separa a los equipos que escalan bien de los que no es la misma en todas las etapas: añade la siguiente capa de complejidad quan­do tengas un cuello de botella medido, no cuando lo anticipes. Instrumenta el sistema desde el principio —tiempos de respuesta, carga de CPU, latencia de consultas de base de datos— para que la señal de “toca escalar esta pieza concreta” sea un dato, no una corazonada. Cada capa de esta progresión (réplicas, caché, CDN, sharding) resuelve un cuello de botella específico y, como contrapartida, añade una fuente nueva de inconsistencia o de complejidad operativa que no compensa pagar antes de tiempo. Diseñar para un millón de usuarios cuando tienes cien no es previsión: es la forma más común de no llegar nunca a los mil.

Compartir