> For the complete documentation index, see [llms.txt](https://ayakaleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ayakaleaf-pro.ayaka.space/blog/es/2026/overleaf-benchmark.md).

# Benchmark de Overleaf: Una investigación profunda de la compilación concurrente de LaTeX en Overleaf (SaaS y autohospedado)

{% file src="/files/54248203939c60bd871e64ccd3aa596b828a90ab" %}

## Resumen

Las implementaciones autogestionadas de Overleaf suelen dimensionarse con una única regla práctica: un núcleo de CPU y un gigabyte de memoria por cada cinco a diez usuarios concurrentes. Mostramos que esta regla no es solo imprecisa, sino estructuralmente errónea, porque asume que una sola dimensión de recursos gobierna la capacidad cuando en realidad lo hacen dos barreras independientes, y porque dos parámetros de software —ninguno de ellos de hardware— dominan el resultado por factores de hasta cuatro.

Medimos una implementación estándar de Ayakaleaf Pro v6.2.2 con compilación en sandbox (TeX Live 2025) en 21 configuraciones de CPU/memoria dentro de invitados QEMU/KVM cuyos núcleos anfitriones están bloqueados a 3.0 GHz. La carga de trabajo es una tesis real de 63 páginas en XeLaTeX compilada simultáneamente por hasta varios cientos de cuentas de usuario distintas. Encontramos que por debajo de 32 GiB de memoria del invitado, el número de núcleos es casi irrelevante —a 16 GiB la capacidad medida de invitados con 4, 8 y 16 vCPU difiere en menos de un 8%— y que la capacidad está gobernada en cambio por una barrera de memoria superlineal que surge de la caché de páginas compartida sobre el árbol de TeX Live.

Para preguntar si estas leyes sobreviven a un cambio de escala de un orden de magnitud, repetimos el barrido en un único servidor de 64 núcleos y 995 GiB. Sostiene 1024 compilaciones frías simultáneas con un 100% de éxito —ocho veces su número de hilos— y nunca alcanzamos su techo. El número útil no es ese techo, sino el codo por debajo de él: la latencia de cola crece entre un 20–40% por cada duplicación hasta $$N=256$$, luego un 190% en $$N=512$$. La capacidad informada como “la mayor concurrencia que no falla” sobreestimaría, por tanto, el punto operativo utilizable por un factor de cuatro. En esa máquina la memoria nunca es el recurso limitante; el límite es la CPU junto con la tasa a la que el demonio de contenedores puede admitir nuevos sandboxes, que se satura cerca de 200 independientemente de cuántas compilaciones se soliciten.

Además identificamos dos efectos a nivel de implementación invisibles para la planificación de capacidad. Primero, CLSI impone un techo codificado de 65 compilaciones simultáneas que no se expone mediante ninguna variable de entorno; más allá de él, los usuarios reciben HTTP 503 de inmediato en lugar de quedar en cola. Segundo, el límite de memoria por contenedor en el runner de Docker ha sido ineficaz desde su introducción en 2018, tanto por magnitud como por ubicación, de modo que un evento de falta de memoria derriba todo el host en lugar de una sola compilación. Elevar el techo de concurrencia y aumentar el tiempo de espera de compilación predeterminado de 180 s a 300 s incrementa la capacidad medida de un invitado de 8 vCPU / 48 GiB de 64 a 268 compilaciones concurrentes —un factor de 4.2 sin coste de hardware.

Por último, mostramos que la concurrencia en este sistema no compra nada salvo reparto de tiempo, y que la carga de trabajo está limitada solo por el reloj. Una ley de degradación ajustada $$T(N)=T\_1\max(1,N/C)^{b}$$ produce $$b=0.914$$, casi una ralentización proporcional perfecta, y un barrido de reloj sobre todo el rango de 1.0–5.5 GHz de la máquina colapsa treinta mediciones sobre $$T=(k/f)\max(1,N/C)$$ con $$k=27.9 GHz·s$$ y una dispersión residual del 5.1%. Un reloj 5.5× compra una aceleración 5.5× sin rendimiento decreciente, que es el sentido en que reloj y núcleos compran cosas distintas: el reloj hace más rápida la compilación de cada usuario, los núcleos solo admiten a más usuarios.

## 1. Introducción

Overleaf es el editor colaborativo de LaTeX dominante, y su distribución en local se despliega ampliamente en universidades y grupos de investigación que no pueden enviar manuscritos inéditos a una nube de terceros. Dimensionar una implementación así es una pregunta práctica recurrente: dado un presupuesto fijo de hardware, ¿cuántas personas pueden realmente pulsar “Recompilar” al mismo tiempo?

La guía oficial es una regla lineal —aproximadamente un núcleo y un gigabyte por cada cinco a diez usuarios concurrentes—, que presupone que la capacidad escala de forma suave y conjunta en ambos recursos. Nuestras mediciones contradicen esto de tres maneras.

### 1.1 La capacidad está gobernada por dos barreras independientes, no una

Una configuración falla o bien porque se agota la memoria, en cuyo caso la propia pila de Overleaf muere y devuelve HTTP 502, o bien porque las compilaciones superan el tiempo de espera del lado del servidor, en cuyo caso CLSI informa `agotado` mientras gigabytes de memoria permanecen sin usar. Estos dos regímenes tienen comportamientos de escalado y remedios totalmente distintos. Añadir núcleos a una configuración limitada por memoria no es solo ineficiente, sino a veces contraproducente: medimos configuraciones en las que aumentar el número de núcleos *reduce* la capacidad, porque más núcleos hacen que las compilaciones concurrentes avancen al mismo ritmo, de modo que sus picos de demanda de memoria coinciden en lugar de entrelazarse.

### 1.2 Los parámetros de software dominan al hardware

El tiempo de espera de compilación es un campo por usuario en MongoDB cuyo valor predeterminado de 180 s limita silenciosamente las configuraciones ligadas a la CPU. Aumentarlo a 300 s multiplica la capacidad medida hasta por 4.2 con el mismo hardware. Independientemente de ello, CLSI rechaza más de 65 compilaciones simultáneas mediante una constante codificada. Cualquier estudio de capacidad —y cualquier despliegue— que no tenga en cuenta ambos está midiendo el software, no la máquina.

### 1.3 La concurrencia es reparto de tiempo, no paralelismo

Debido a que una compilación de LaTeX es de un solo hilo, atender $$N$$ usuarios simultáneos en $$C$$ núcleos no hace que el sistema termine antes; hace que cada usuario espere proporcionalmente más. Por tanto, la pregunta “cuántos usuarios concurrentes se soportan” está mal planteada hasta que se fija cuánto tiempo está dispuesto a esperar un usuario. Hacemos explícita esta dependencia y la cuantificamos.

### 1.4 Contribuciones

* Una matriz de capacidad sobre 21 configuraciones de CPU/memoria medida en condiciones con reloj bloqueado y verificación repetida, con la restricción limitante identificada para cada configuración a partir de su firma de fallo.
* Dos modelos ajustados: un modelo de capacidad que separa una barrera de memoria superlineal de un techo de CPU, y un modelo de latencia que establece un comportamiento puro de reparto de tiempo.
* Identificación y confirmación experimental de dos problemas de implementación en el sistema desplegado, incluido un límite de memoria de contenedor que ha estado inoperativo desde 2018.
* Una cuantificación del compromiso entre tiempo de compilación y capacidad, que sostenemos que debe indicarse junto con cualquier cifra de concurrencia.

## 2. Antecedentes

### 2.1 Ruta de compilación

Una solicitud de compilación de Overleaf viaja `web` $$\rightarrow$$ `clsi` $$\rightarrow$$ a un contenedor de compilación. En un despliegue con compilaciones en sandbox (`SIBLING_CONTAINERS_ENABLED=true`), CLSI no ejecuta `latexmk` en proceso; pide al demonio Docker del host, accesible a través de un socket montado por bind, que inicie un contenedor nuevo a partir de una imagen de TeX Live con el directorio del proyecto montado por bind en `/compile`. Por tanto, una compilación es un contenedor efímero que ejecuta un único `latexmk` proceso.

De esto se siguen tres consecuencias, y las tres moldean las mediciones de este trabajo. Primero, la unidad de trabajo es un proceso de un solo hilo: XeLaTeX no se paraleliza. Segundo, el aislamiento de recursos por compilación es el que solicite el runner de Docker —demostramos en §6.2 que, en la práctica, no solicita nada. Tercero, el conjunto de trabajo no está dominado por el documento, sino por el árbol de TeX Live, un corpus de solo lectura de unos 32 GiB del que cada compilación concurrente lee y que, por tanto, comparte a través de la caché de páginas del host. Esta compartición es el origen del escalado superlineal de memoria que observamos.

### 2.2 Activación de compilaciones en sandbox

Overleaf de la edición comunitaria se ejecuta `latexmk` dentro del propio contenedor de la aplicación. Ayakaleaf Pro, al igual que Overleaf Server Pro, puede en cambio ejecutar cada compilación en un contenedor *hermano* —un contenedor iniciado por la aplicación en el *del host* demonio Docker en lugar de anidado dentro del contenedor de la aplicación. Dos ajustes del toolkit activan esto:

```ini
# config/overleaf.rc
SERVER_PRO=true
SIBLING_CONTAINERS_ENABLED=true
DOCKER_SOCKET_PATH=/var/run/docker.sock

# config/variables.env
TEX_LIVE_DOCKER_IMAGE=ghcr.io/ayaka-notes/texlive-full:2025.1
ALL_TEX_LIVE_DOCKER_IMAGES=ghcr.io/ayaka-notes/texlive-full:2025.1
```

El toolkit monta por bind el socket Docker del host dentro del contenedor de la aplicación y traduce esto a las variables de entorno que lee CLSI: `SANDBOXED_COMPILES=true`, `SANDBOXED_COMPILES_SIBLING_CONTAINERS=true`, y `SANDBOXED_COMPILES_HOST_DIR`, cuyo último elemento es la ruta *del host* del directorio de compilación. Esa ruta importa: como el demonio que inicia el contenedor de compilación es el del host, el montaje por bind que se le entrega debe resolverse en el espacio de nombres del host, no en el del contenedor de la aplicación. El `config/env.sh` de Server Pro además fuerza `TEXLIVE_IMAGE_USER=www-data` en este modo para que los archivos escritos por el contenedor de compilación tengan una propiedad coherente.

La verificación es directa: durante una compilación el host muestra un contenedor llamado `project-{projectId}-{userId}-{hash}` en ejecución `latexmk` a partir de la imagen de TeX Live, saliendo con 0. Esta es la unidad cuya multiplicidad medimos a lo largo de todo el trabajo, y cuya ausencia completa de límites de recursos informamos en §6.2.

**Por qué esto importa para el estudio.**

Los contenedores hermanos hacen la medición limpia —cada compilación es una entidad del sistema operativo observable y programada de forma independiente—, pero también significan que es el kernel invitado, no Overleaf, el que arbitra CPU y memoria entre compilaciones. Por tanto, toda ley de escalado de este trabajo es una propiedad del planificador de Linux aplicado a $$N$$ procesos de un solo hilo, que es precisamente por lo que es tan regular.

<figure><img src="/files/1cefcfbe63b19bb3bc971463e7a4ae414e3d229f" alt=""><figcaption></figcaption></figure>

**Figura 1.** Una solicitud de compilación, trazada a través de los microservicios de la edición comunitaria. La división en pasos importa para la capacidad: el texto del documento se copia en el cuerpo de la solicitud, mientras que los recursos binarios se pasan por referencia y se descargan por `clsi`. Ninguno domina: el coste de compilación de un proyecto lo fija el árbol de TeX Live de 32 GiB del que cada compilación concurrente lee a través de la caché de páginas compartida.

<figure><img src="/files/218c1c1800f805df4007751b0430c2f03c6e1aa8" alt=""><figcaption></figcaption></figure>

**Figura 2.** Tres topologías de despliegue y dónde cae en cada una el techo de compilación por instancia; los paneles apilados denotan replicación. La constante de 65 compilaciones protege *uno* CLSI, así que la flota SaaS la multiplica por instancias y zonas (a), y el escalado horizontal que soportan Server Pro y Ayakaleaf Pro la multiplica por instancias (c) —a costa de MongoDB central, Redis y almacenamiento compatible con S3, un equilibrador de carga con afinidad de sesión por cookie (la salida de compilación se escribe en disco local de la instancia, de modo que una compilación y su descarga posterior del PDF deben caer en la misma instancia), y un `git-bridge`singleton. El valor predeterminado del toolkit (b), que es lo que medimos, tiene un multiplicador de uno, así que una constante dimensionada para un miembro de una flota se convierte en el techo de toda la instalación.

<figure><img src="/files/1bef591ce717ca1defb0972e98eecaeeb349a45b" alt=""><figcaption></figcaption></figure>

**Figura 3.** Selección de partición en `clsi-cache`. Un proyecto se asigna mediante $$\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}|$$, es decir, el espacio de hash se corta en tantos sectores iguales como particiones haya. Esto es *módulo* hashing, no hashing consistente basado en anillo: al crecer la flota de tres particiones a cuatro se redistribuye todo el espacio y se reasigna prácticamente cada proyecto (a, b). Precisamente por eso la implementación necesita una rampa explícita de redistribución en línea, moviendo a lo largo de una ventana de tiempo una fracción linealmente creciente de proyectos desde `currentShards` a `desiredShards` , en lugar del $$K/n$$ movimiento que daría un anillo de hashing consistente. Cuando se dispara el disyuntor de una partición, la sal $$i$$ se incrementa y la partición se elimina de la lista de candidatas, de modo que la búsqueda continúa en lugar de fallar (c).

### 2.3 Los dos modos de fallo

Cada configuración que medimos falla exactamente de una de dos maneras, y la distinción es visible en el estado de respuesta en lugar de inferirse:

* **Agotamiento de memoria** — la propia pila de Overleaf deja de responder y la solicitud devuelve **HTTP 502**. La memoria disponible del invitado en el nivel de fallo suele estar por debajo de 500 MiB.
* **Tiempo de espera de compilación** — CLSI termina la compilación en el tiempo de espera por usuario e informa estado `agotado`. La memoria disponible en el nivel de fallo suele ser de varios gigabytes.

Clasificamos cada configuración por esta firma en lugar de por una heurística sobre proporciones de recursos, lo que hace que la pregunta “qué barrera alcanzamos” pueda responderse a partir de los propios datos.

## 3. Metodología

### 3.1 Banco de pruebas y control del reloj

Todos los invitados se ejecutan bajo QEMU/KVM en un único host Intel Core i9-14900K con 62 GiB de RAM y almacenamiento NVMe. El invitado es Ubuntu 24.04 con Docker 29.7 y el Overleaf Toolkit desplegando Ayakaleaf Pro v6.2.2 con compilaciones en sandbox contra `texlive-full:2025.1`.

Una CPU de escritorio común es un mal sustituto de un servidor salvo que su reloj esté controlado. KVM no ofrece ningún mecanismo para fijar un reloj virtual: una vCPU es un hilo del host y corre a la frecuencia a la que corra el núcleo del host. Por ello limitamos directamente el host, desactivando turbo y fijando `scaling_max_freq` a 3.0 GHz en cada núcleo, y fijamos las vCPU del invitado a núcleos P físicos con `taskset`. La distinción importa en una CPU híbrida: los núcleos E de este chip tienen una frecuencia base de 2.4 GHz y *no pueden* alcanzar 3.0 GHz una vez desactivado turbo, así que una ejecución que se desvíe hacia ellos mide en silencio una máquina más lenta. Bajo carga completa verificamos exactamente 3000 MHz en los dieciséis hilos fijados. Un script guardián afirma este invariante antes de cada benchmark y se niega a iniciarlo en caso contrario; detectó un reinicio silencioso del gobernador durante el estudio.

### 3.2 Un segundo banco de pruebas: un único runner grande

La matriz QEMU aísla una variable cada vez, pero se queda en dieciséis hilos fijados. Para preguntar si las mismas leyes siguen vigentes a un orden de magnitud superior, repetimos el barrido de concurrencia en un único servidor grande: un AMD EPYC 7773X (Milan-X, 64 núcleos / 128 hilos, 768 MiB de L3) con 995 GiB de RAM, ejecutando la misma imagen de Ayakaleaf Pro v6.2.2 contra el mismo `texlive-full:2025.1`. A diferencia de los invitados QEMU, esta máquina no tiene el reloj fijado: es un servidor de clase producción y la medimos como tal.

Fueron necesarias dos precauciones operativas y merece la pena expresarlas, porque sin ellas el experimento mide el arnés en lugar del servidor. Primero, cada contenedor se confinó a un fragmento `systemd` con `MemoryMax=940 GiB`, de modo que un barrido desbocado agota un cgroup y no el host. Segundo, las compilaciones en sandbox las crea el demonio del host y cada una ensucia su propia capa copy-on-write —medida en 116 MiB por contenedor aunque la imagen base de 20.6 GiB se comparte—, así que la raíz de datos de Docker se trasladó a un dispositivo NVMe dedicado. Un barrido a $$N=1024$$ escribe aproximadamente 119 GiB de capas temporales, lo que no cabe en un sistema de archivos raíz estándar.

### 3.3 Carga de trabajo

El documento es una tesis real de maestría de 63 páginas (plantilla SJTU) compilada con XeLaTeX mediante `latexmk`, que contiene figuras TikZ, `biblatex` procesamiento de bibliografía y recursos PDF incrustados —es decir, una carga realista y no sintética. Una sola compilación en un invitado sin carga tarda 8.6–9.8 s en todas las configuraciones, lo que usamos como línea base sin carga $$T\_1$$.

### 3.4 Generación de carga

Creamos 512 cuentas de usuario reales y damos a cada una su propia copia del proyecto, de modo que las compilaciones concurrentes contiendan exactamente como lo harían usuarios independientes y no compartiendo un bloqueo de proyecto. Las solicitudes se emiten desde el host contra el puerto reenviado del invitado, de manera que la generación de carga no consume CPU del invitado.

La concurrencia es *simultánea*, no escalonada. Cada sesión se establece primero —inicio de sesión, token CSRF, selección de compilador— y solo entonces cada hilo duerme hasta un instante común del reloj, calculado una sola vez y compartido, antes de emitir su `POST /project/:id/compile`. La distinción no es pedante. Una rampa escalonada mide el rendimiento bajo una cola estable; un estallido simultáneo mide lo que ocurre cuando un aula llena de estudiantes pulsa el mismo botón tras el mismo anuncio de fecha límite, que es el caso que realmente temen los operadores. Ambos difieren en más de un factor constante, porque el segundo llena la cola de compilación más rápido de lo que el demonio puede vaciarla.

Hubo que eliminar cuatro obstáculos prácticos antes de poder entregar fielmente ese estallido. Merece la pena registrarlos, porque cada uno degrada en silencio el experimento hasta convertirlo en una medición del arnés y no del servidor.

#### 3.4.1 Dos limitadores de tasa, no uno

Overleaf limita los inicios de sesión por dirección de origen —20 intentos por minuto— y todo nuestro tráfico procede de un único host. Asignar a cada usuario simulado una dirección distinta `X-Forwarded-For` elimina ese límite pero enseguida choca con un segundo, más grueso: un presupuesto por subred de unos 200 por minuto. Por tanto, distribuir usuarios en un bloque contiguo falla en la cuenta 201. En su lugar derivamos la dirección sintética del índice de usuario para que los usuarios consecutivos caigan en distintas `/24`subredes,

$$
\texttt{203.};\big\lfloor i/250 \big\rfloor \bmod 100 + 1\texttt{.};
i \bmod 250 + 1\texttt{.}; i \bmod 200 + 10 ,
$$

lo que mantiene holgados ambos limitadores para toda la población de 1024.

#### 3.4.2 La cabecera inyectada se descarta por defecto

Establecer la cabecera no basta. Express solo respeta `X-Forwarded-For` para pares en los que se le ha dicho que confíe, y el `trustedProxyIps` de Overleaf tiene por defecto `loopback`. Como el generador de carga alcanza la aplicación a través del puente del contenedor y no por la interfaz loopback, la cabecera se analiza y luego se descarta, y cada usuario simulado vuelve a colapsar sobre una sola dirección. El síntoma es una ola de `HTTP 429` exactamente en el vigésimo inicio de sesión, lo que es fácil confundir con una sobrecarga del servidor. La red de pasarela debe añadirse explícitamente a la cadena de confianza; en el despliegue en clúster de §4.3 deben añadirse también los CIDR del pod y del servicio.

#### 3.4.3 Un equilibrador de carga sobrescribirá la cabecera que se le pidió preservar

Cuando la instancia está detrás de un proxy, la opción convencional `option forwardfor` *añade* la dirección real del cliente a la cadena, lo cual es el comportamiento correcto para producción y exactamente incorrecto aquí: la dirección sintética queda desplazada por la del propio generador de carga. La directiva debe calificarse como `option forwardfor if-none`, de modo que el proxy añada un valor solo cuando el cliente no haya suministrado ninguno.

#### 3.4.4 El cliente se queda sin descriptores de archivo antes de que el servidor se quede sin capacidad

En $$N=1024$$ el generador mantiene más de mil sockets simultáneos, y el límite blando predeterminado de 1024 descriptores se alcanza durante la preparación de la sesión en lugar de durante la medición. El fallo es silencioso: tres sesiones no llegan a establecerse y la ejecución informa 1021 en lugar de 1024, mientras que un hilo de muestreo que invoca procesos de shell para contar contenedores muere con `EMFILE` y trunca la telemetría sin avisar. El límite blando debe elevarse en el generador —el límite duro en nuestro host ya era 1048576— y la ejecución repetirse. Informamos ambas ejecuciones en §4.3: la corregida completa 1024 de 1024 con una mediana a 1.2 s de la truncada, razón por la que tratamos la primera como utilizable pero no autoritativa.

### 3.5 Protocolo de medición

Varias decisiones metodológicas resultaron necesarias para la reproducibilidad.

#### 3.5.1 Calentamiento

En un invitado recién arrancado la caché de páginas está vacía y las primeras compilaciones miden E/S de arranque en frío en lugar de capacidad en estado estable: la misma configuración de 2 vCPU / 2 GiB produce 36.5 s en frío y 9.8 s en caliente, un factor de 3.7. Por tanto, cada configuración realiza dos calentamientos de una sola compilación desechados después del arranque.

#### 3.5.2 Criterio de aprobación

Un nivel de concurrencia solo aprueba si *cada* compilación tiene éxito y el nivel resiste una repetición. Esto es más estricto que un umbral de tasa de éxito y importa: a 4 vCPU / 16 GiB un nivel de 32 aprobó una vez con una mediana de 80.2 s y luego agotó el tiempo en las 32 compilaciones al repetirse, así que informamos 31.

#### 3.5.3 Búsqueda

Los niveles se localizan mediante acotación exponencial a partir de una semilla predicha por el modelo, seguida de bisección entera exacta. Como el criterio es de todo o nada, un nivel queda decidido por su primer fallo, así que abandonamos las solicitudes restantes en vuelo una vez falla una —salvo en niveles pequeños, donde las compilaciones abandonadas fijan un invitado pequeño con tanta fuerza que nunca se recupera.

#### 3.5.4 Aislamiento entre niveles

Los contenedores de compilación se vacían, y la aplicación web se consulta hasta que vuelve a responder, antes de que empiece el siguiente nivel. Sin esto, un nivel posterior a un fallo registra una falsa falla de cero sesiones.

#### 3.5.5 Higiene del host

Se apagaron máquinas virtuales no relacionadas en el host: con 24 GiB de memoria del host comprometidos en otro lugar, la misma configuración de invitado informó una carga promedio de 11.7 en lugar de 3.2 con la misma concurrencia. La presión de memoria del host se propaga al invitado e invalida la medición.

## 4. Resultados

### 4.1 La matriz de capacidad

La Tabla 1 y la Figura 4 muestran el techo medido para cada configuración. Leerla en una fila es la primera sorpresa. A 4 GiB los invitados de 2, 4 y 8 vCPU alcanzan todos exactamente 9 —cuadruplicar los núcleos no cambia nada. A 16 GiB alcanzan 54, 45 y 57: pasar de 4 a 16 núcleos solo compra un 6%, y el invitado de 8 núcleos es de hecho *peor* que el de 4 núcleos (§5.2). Solo a 48 GiB el número de núcleos separa las configuraciones de forma decisiva: 143, 268 y 331.

<figure><img src="/files/cd56899f74e241b11248d4ab4a119d9e9fbf1b0e" alt=""><figcaption></figcaption></figure>

**Figura 4.** Capacidad medida a través de la matriz de configuraciones. (a) Cada configuración como una barra, agrupada por memoria y coloreada por número de núcleos; las barras sólidas están limitadas por memoria (el invitado muere por agotamiento de memoria) y las rayadas están limitadas por CPU (las compilaciones agotan el tiempo con memoria de sobra). Leer un grupo de izquierda a derecha muestra lo poco que compra el número de núcleos por debajo de 16 GiB; leer a través de los grupos muestra el rendimiento superlineal de la memoria. (b) Los mismos puntos frente al modelo ajustado $$N\_{\max}=\min(0.69R^{1.60},,26.4C)$$; la línea discontinua es la barrera de memoria y las horizontales punteadas son los techos de CPU por número de núcleos. Una configuración está limitada por la primera de las dos que encuentre.

Leerla hacia abajo por una columna es la segunda: con núcleos fijos, la capacidad crece superlinealmente con la memoria, aproximadamente como $$R^{1.6}$$, por la razón de caché de páginas desarrollada en §5.1.

| memoria | 2 vCPU |  4 vCPU |  8 vCPU | 16 vCPU |
| ------: | -----: | ------: | ------: | ------: |
|   2 GiB |      1 |       1 |       1 |       — |
|   3 GiB |      4 |       5 |       6 |       — |
|   4 GiB |      9 |       9 |       9 |       — |
|   8 GiB |     21 |      22 |      26 |       — |
|  16 GiB |      — |      54 |  **45** |      57 |
|  32 GiB |      — | **145** |     135 |     141 |
|  48 GiB |      — | **143** | **268** | **331** |

**Tabla 1.** Número máximo de compilaciones simultáneas que completan con éxito, medido con un tiempo de espera de compilación de 300 s y levantado el techo de concurrencia de CLSI. **Negrita** marca una configuración limitada por CPU (las compilaciones agotan el tiempo con memoria de sobra); el resto están limitadas por memoria (la pila muere con HTTP 502). La fila de 2 GiB lleva la corrección discutida en §5.2.

### 4.2 La concurrencia es reparto de tiempo

La Figura 5 barre todos los niveles de concurrencia en un invitado fijo de 8 vCPU / 16 GiB. Dos regímenes están separados por un codo pronunciado exactamente en una compilación por núcleo. Por debajo de él, el tiempo medio de compilación es plano —pasa de 8.7 s en $$N=1$$ a 9.1 s en $$N=C=8$$, un cambio del 5%. Por encima, el tiempo crece en estricta proporción con $$N/C$$: en $$N=16,24$$ medimos 18.5 s y 27.1 s, es decir, una razón de $$1:2.13:3.12$$ frente a un ideal $$1:2:3$$.

<figure><img src="/files/5974fb22012fa6b2d5dec08d557e3a940b81fe46" alt=""><figcaption></figcaption></figure>

**Figura 5.** Latencia de compilación frente a concurrencia con hardware fijo. El codo está en $$N=C$$; más allá de él la ralentización medida sigue $$N/C$$ con un margen del 5–7%. Los quince niveles tuvieron éxito por completo.

<figure><img src="/files/9c477548f94c0dab2732bc42a106f2098ec85b0e" alt=""><figcaption></figcaption></figure>

**Figura 6.** Latencia de compilación frente a concurrencia para varias configuraciones. Cada panel mantiene fijo el hardware y barre la carga ofrecida; la regla vertical marca $$N=C$$. Las curvas son planas a su izquierda y lineales en $$N/C$$ a su derecha, que es la firma del reparto de tiempo y no de la contención: el trabajo no se vuelve más caro, simplemente espera su turno.

Ajustar $$T(N)=T\_1\max(1,N/C)^{b}$$ sobre todas las mediciones exitosas del estudio da $$b=0.914$$ ($$R^2\_{\log}=0.904$$, $$n=81$$). Un exponente indistinguible de la unidad es la afirmación cuantitativa de que una compilación es una unidad de trabajo de un solo hilo, ligada a la CPU, y de que la concurrencia ni ayuda ni perjudica más allá de dividir los núcleos. La corolaria práctica es incómoda para la planificación de capacidad: una configuración puede absorber un número arbitrario de usuarios sin *fallar* mientras hace esperar a cada uno proporcionalmente más. En $$N=56$$ en este invitado todas las compilaciones siguen teniendo éxito, pero cada usuario espera 64.8 s en lugar de 8.7 s.

### 4.3 Escalado vertical a 1024 compilaciones concurrentes

La Tabla 2 y la Figura 7 informan el barrido en el gran runner. Cada nivel es una compilación *fría* : antes de cada nivel limpiamos el directorio de compilación y la caché CLSI de cada proyecto participante mediante `DELETE /project/:id/output`, de modo que ningún nivel se beneficia del trabajo realizado por el nivel inferior. La línea base de una sola compilación en esta máquina es 28.8 s, que es la cifra en frío y no debe compararse con la línea base estable de 8.6–9.8 s usada antes; la línea base en frío en los invitados QEMU es 28.3 s, así que por hilo las dos máquinas están dentro de un dos por ciento entre sí para esta carga de trabajo.

| $$N$$ |     éxito | $$p\_{50}$$ | $$p\_{95}$$ | $$p\_{50}/T\_1$$ | contenedores pico |
| ----: | --------: | ----------: | ----------: | ---------------: | ----------------: |
|    64 |     64/64 |      55.0 s |      55.1 s |             1.9× |                 — |
|   128 |   128/128 |      73.6 s |      74.4 s |             2.6× |                 — |
|   192 |   192/192 |      80.2 s |      87.3 s |             2.8× |                 — |
|   256 |   256/256 |      69.8 s |     121.2 s |             2.4× |               151 |
|   512 |   512/512 |     179.2 s |     344.6 s |             6.2× |               205 |
|  1024 | 1024/1024 |     280.2 s |     468.9 s |             9.7× |               179 |

**Tabla 2.** Barrido de concurrencia en un EPYC 7773X (64 núcleos / 128 hilos, 995 GiB). Todos los niveles en frío; línea base 28.8 s. Contenedores pico es el número máximo de sandboxes vivos simultáneamente.

<figure><img src="/files/58286be0838878781b86d88180b5ce87e50be678" alt=""><figcaption></figcaption></figure>

**Figura 7.** Escalado vertical en un gran runner. (a) Latencia frente a concurrencia ofrecida; la región sombreada marca el régimen más allá del codo. (b) El número de sandboxes realmente vivos nunca sigue al número solicitado —se satura cerca de 200— mientras que el cgroup de compilación nunca usa más de una quinta parte de su tope.

#### 4.3.1 La máquina nunca falla

Cada nivel completa al 100%, incluido $$N=1024$$ —ocho veces el número de hilos. No encontramos el techo de capacidad de esta máquina; nos quedamos sin paciencia antes de que se quedara sin margen. Esta es la primera configuración del estudio en la que la restricción limitante no es la memoria: en $$N=1024$$ el cgroup de compilación alcanza un pico de 184 GiB, una quinta parte de su tope de 940 GiB, mientras la CPU está al 100% de utilización con un promedio de carga de 166.

#### 4.3.2 La degradación es sublineal porque la admisión está limitada por tasa

El reparto de tiempo ingenuo predice que $$8\times$$ los hilos cuestan $$8\times$$ la latencia. El coste medido es $$9.7\times$$ en relación con una sola compilación, pero solo $$3.8\times$$ en relación con $$N=128$$ —para un aumento de ocho veces en la carga ofrecida. La razón se ve en la Figura 7(b) y en la última columna de la Tabla 2: aunque se emiten 1024 solicitudes simultáneamente, el número de sandboxes realmente vivos nunca supera 205. El demonio no puede crear contenedores tan rápido como los clientes los piden, así que las solicitudes quedan en cola en la admisión en lugar de competir dentro de la CPU. La cola es lo que salva la cola larga aquí, y lo hace por accidente.

#### 4.3.3 El codo está en 512, no en el punto de fallo

Entre $$N=256$$ y $$N=512$$ el $$p\_{95}$$ la latencia aumenta $$2.9\times$$ por una duplicación de la carga; cada duplicación anterior costó entre $$1.2\times$$ y $$1.4\times$$. La capacidad expresada como “la mayor $$N$$ que no falla” informaría 1024 y sería inútil para un operador: en ese punto la espera en cola es de casi ocho minutos.

### 4.4 El tiempo de compilación es inversamente proporcional al reloj

Dado que la carga de trabajo está limitada por la CPU, su coste debería escalar como $$1/f$$. Lo comprobamos directamente barriendo el reloj del host a lo largo de todo el rango de la máquina, 1.0–5.5 GHz en diez pasos, sobre un invitado por lo demás inalterado (Figura 8). El tiempo de una sola compilación pasa de 26.5 s a 4.8 s: un reloj 5.5× compra una aceleración 5.5× sin ningún rendimiento decreciente en todo el rango. El producto $$T!\cdot!f$$ es constante dentro de un 2% en los diez relojes.

Normalizar por la cuota de núcleo colapsa las treinta mediciones —tres niveles de concurrencia en diez relojes— sobre una única constante:

$$
T(N,f) ;=; \frac{k}{f},\max!\left(1,\frac{N}{C}\right),
\qquad k = 27.9\ \mathrm{GHz\cdot s}
$$

con una dispersión residual del 5.1% en un rango en el que el propio reloj varía 5.5×. La ausencia de curvatura es, en sí misma, el resultado: si la carga hubiera estado limitada por ancho de banda de memoria o por E/S, $$T$$ se aplanaría a reloj alto cuando la CPU superara al otro recurso.

<figure><img src="/files/e4a62ded00b8dabba012a42aaed42de43e57475f" alt=""><figcaption></figcaption></figure>

**Figura 8.** Barrido de reloj. (a) $$T=k/f$$ con la hipérbola ajustada. (b) Tras dividir por $$\max(1,N/C)$$ todos los puntos colapsan sobre una sola constante, confirmando la Ecuación (1).

La Ecuación (1) tiene una consecuencia directa para la adquisición que es fácil de enunciar y fácil de interpretar mal: *el reloj mejora la experiencia de cada usuario individual, el número de núcleos solo admite a más de ellos*. Una máquina con un 20% más de reloj compila un 20% más rápido para todo el mundo, sin rendimiento decreciente; el doble de núcleos no hace que la compilación de nadie sea más rápida en absoluto.

## 5. Análisis

### 5.1 Dos barreras, ajustadas por separado

Cada configuración se clasifica por su firma de fallo (§2.3), y la barrera de memoria y el techo de CPU se ajustan entonces solo sobre las configuraciones que realmente los alcanzan:

$$
N\_{\max} = \min\left(A R^{p},; k\_c C\right)
$$

con $$R$$ en gibibytes y $$C$$ en vCPUs.

El exponente del muro de memoria es consistentemente superlineal, $$p>1$$: el costo marginal de memoria de una compilación concurrente adicional *disminuye* a medida que crece la memoria total, desde aproximadamente 312 MiB por compilación en un huésped de 3 GiB hasta unos 194 MiB en uno de 32 GiB. El mecanismo es la caché de páginas compartida sobre el árbol de TeX Live descrito en §2.1: las compilaciones concurrentes leen archivos de fuentes y macros superpuestos, de modo que una caché mayor se amortiza entre más de ellas. Por eso la regla ingenua de «un gigabyte por cada cinco usuarios» subestima las máquinas grandes y sobrestima las pequeñas.

<figure><img src="/files/bc0feb138dc851cc54b0881bd7990b23de718468" alt=""><figcaption></figcaption></figure>

**Figura 9.** Los mismos datos como dos superficies sobre el $$(C,R)$$ plano. (a) Capacidad: la superficie ajustada es una cresta, no un plano — asciende con fuerza con la memoria y es casi plana a lo largo del eje de núcleos hasta que la memoria deja de ser vinculante, razón por la cual la fila de 48 GiB es la única en la que el número de núcleos separa las configuraciones. (b) Latencia frente a concurrencia para cada configuración, con el ajuste $$T=12.6,(N/C)^{0.91}$$ mostrado con línea discontinua y el tiempo de espera de 180 s dibujado como un plano. Una configuración falla donde su curva sólida atraviesa ese plano, lo que hace visible cuán directamente la configuración del tiempo de espera determina la capacidad informada.

### 5.2 Dónde más núcleos empeoran las cosas

La ecuación (2) es un mínimo de dos términos y, por tanto, es monótona en $$C$$, pero las mediciones no lo son. Observamos dos inversiones en las que añadir núcleos *redujo* la capacidad: a 16 GiB (54 frente a 45) y a 32 GiB (145 frente a 135). Ambas ocurren en el régimen limitado por memoria, y el mecanismo es el mismo en cada caso: con más núcleos, las compilaciones concurrentes avanzan al unísono y alcanzan su tamaño residente máximo al mismo tiempo, mientras que con menos núcleos el planificador las entrelaza y los picos quedan escalonados. En un huésped cuyo margen de memoria ya es precario, el escalonamiento es lo que lo mantiene con vida. Un modelo de capacidad basado en el uso promedio de recursos no puede expresar esto; es una propiedad de la *coincidencia* de picos.

Una tercera inversión aparente, a 2 GiB, la descartamos ahora. El registro de búsqueda informa capacidad 2 con 2 vCPU pero 1 con 4 y 8 vCPU, lo que parece el mismo efecto. Al volver a examinar los barridos brutos vemos algo más simple: a 2 GiB el nivel $$N=2$$ tuvo éxito al primer intento en los tres recuentos de núcleos y luego falló en su ejecución de confirmación en dos de los tres. El nivel no es una capacidad sino un lanzamiento de moneda, y la entrada de 2 vCPU es el lanzamiento que cayó de ese modo. Por tanto, informamos el valor reproducible, 1, en los tres recuentos de núcleos y no sacamos ninguna conclusión de la diferencia. Registramos la corrección aquí en lugar de repetir silenciosamente la tabla, porque la lectura descartada es el tipo de lectura que habría respaldado una afirmación interesante.

## 6. Resultados de la implementación

### 6.1 Un límite de concurrencia codificado a mano

En huéspedes suficientemente grandes, la capacidad se detuvo exactamente en 65 compilaciones simultáneas independientemente de la concurrencia solicitada: en $$N=66,80,96,128$$ medimos $$65$$ éxitos y $$1,15,31,63$$ inmediatas `no disponibles` respuestas, con el número de contenedores fijado en 65, varios gigabytes de memoria sin usar y el tiempo medio de compilación estable en 77 s — muy por debajo de cualquier tiempo de espera.

La causa es una constante en CLSI:

```javascript
// services/clsi/config/settings.defaults.cjs:110
compileConcurrencyLimit: isSpotInstance ? 32 : 64,

// services/clsi/app/js/LockManager.js
if (LOCKS.size <= Settings.compileConcurrencyLimit) return   // <= admite 64+1
throw new Errors.TooManyCompileRequestsError(...)
```

La comparación no es estricta, así que el límite efectivo es $$64+1=65$$exactamente, coincidiendo con la medición. Las solicitudes excedentes reciben **HTTP 503** — son *rechazadas*y no se encolan, así que desde el lado del usuario el botón de compilar simplemente falla. A diferencia de cualquier otro parámetro ajustable en el mismo archivo, este no lee ninguna variable de entorno; se introdujo aguas arriba en agosto de 2024 y solo puede cambiarse modificando la imagen. Con el límite aumentado, el mismo huésped de 16 vCPU / 32 GiB que informaba `éxito=65, no disponible=15` en $$N=80$$ informó en cambio `éxito=80`.

### 6.2 Un límite de memoria del contenedor inoperante

Inspeccionar un contenedor de compilación en vivo no muestra ninguna aislación de recursos en absoluto:

```
Memory=0  NanoCpus=0  CpuShares=0  CpuQuota=0  CpusetCpus=[]
Ulimits=[{Name:cpu Soft:305 Hard:310}]
```

La ausencia de cualquier cuota de CPU es deliberada y explica por qué el exponente de reparto de tiempo de §4.2 es tan limpio: nada distorsiona la competencia entre compilaciones. La ausencia de un *memoria* límite, sin embargo, no es deliberada. El ejecutor de Docker sí solicita uno:

```javascript
// services/clsi/app/js/DockerRunner.mjs:262
Memory: 1024 * 1024 * 1024 * 1024, // 1 Gb
```

Esto está mal por partida doble. El valor es $$1024^4=1 tebiB$$ donde el comentario pretende $$1024^3$$; y el campo se coloca en el nivel superior de las opciones de creación en lugar de dentro de `HostConfig`, donde la API de Docker lo espera, por lo que se descarta — lo que el observado `Memory=0` confirma. Ambos errores están presentes en el commit que introdujo el archivo (`9a519f0d3d`, marzo de 2018) y sobrevivieron a la conversión desde CoffeeScript, a un reformateo en todo el repositorio y a una migración de CJS a ESM, ninguno de los cuales revisa la semántica. En particular `MAX_OUTPUT = 1024 * 1024 // 1MB` en el mismo commit es correcta, lo que indica un desliz más que un malentendido.

La consecuencia es visible en nuestras mediciones con poca memoria. Como las compilaciones no tienen límite, el agotamiento de memoria no se manifiesta como que Docker termine un contenedor infractor; derriba todo el huésped. En la configuración de 2 vCPU / 2 GiB observamos que la sesión SSH de monitorización quedó bloqueada durante 300 s, una carga media de 68 en dos núcleos y, finalmente, el huésped reiniciándose a sí mismo. Un límite por contenedor que funcionara degradaría de forma mucho más elegante: la compilación demasiado grande fallaría y el servicio sobreviviría.

El único límite que sí entra en efecto es `RLIMIT_CPU`, fijado en $$\text{timeout}+5$$ segundos. Limita *el tiempo de CPU* y no el tiempo de reloj, y una sola compilación consume solo unos 9 s de CPU, así que nunca llega a ser vinculante en ninguna concurrencia; protege contra entradas patológicas como una macro desbocada. Sin embargo, es un oráculo útil: observar `Soft:305` confirma que una configuración de tiempo de espera de 300 s realmente se ha propagado al contenedor.

### 6.3 El tiempo de espera de compilación es el ajuste dominante

El campo por usuario `features.compileTimeout` tiene por defecto 180 s. Para cualquier configuración limitada por CPU esto no es un margen de seguridad sino una configuración de capacidad, porque una máquina que sigue calculando correctamente se declara fallida. Subirlo a 300 s — una única actualización de MongoDB — cambia la capacidad medida hasta en un factor de 4.2 (Tabla 3). El techo es de 600 s, aplicado por `RequestParser.MAX_TIMEOUT`, por encima del cual el valor se trunca silenciosamente.

| Configuración    | 180 s | 300 s |          Razón | Restricción vinculante             |
| ---------------- | ----: | ----: | -------------: | ---------------------------------- |
| 8 vCPU / 48 GiB  |    64 |   268 | $$4.19\times$$ | CPU, 28 GiB libres                 |
| 4 vCPU / 32 GiB  |    47 |   145 | $$3.09\times$$ | CPU, 30 GiB libres                 |
| 4 vCPU / 48 GiB  |    63 |   143 | $$2.27\times$$ | el tiempo de CPU                   |
| 4 vCPU / 16 GiB  |    31 |    54 | $$1.74\times$$ | el tiempo de CPU                   |
| 2 vCPU / 8 GiB   |    15 |    21 | $$1.40\times$$ | el tiempo de CPU                   |
| 8 vCPU / 32 GiB  |   127 |   135 | $$1.06\times$$ | choca luego con el muro de memoria |
| 8 vCPU / 16 GiB  |    56 |    45 | $$0.80\times$$ | memoria                            |
| 16 vCPU / 32 GiB |   159 |   141 | $$0.89\times$$ | memoria                            |

**Tabla 3.** Efecto del tiempo de espera de compilación en la capacidad medida.

Las dos últimas filas son la mitad contraintuitiva del resultado y la razón por la que volvimos a medir cada configuración bajo un solo tiempo de espera. Para *memoria*configuraciones limitadas, un tiempo de espera más largo *reduce* aumenta la capacidad, porque cada compilación mantiene su conjunto residente durante más tiempo y se solapan más de ellas. Por tanto, una cifra de capacidad no significa nada sin indicar el tiempo de espera bajo el que se midió, y ambas cosas no pueden mezclarse dentro de una sola tabla.

## 7. Trabajo relacionado

### 7.1 Orientación del proveedor

La propia documentación de hardware de Overleaf afirma los hechos cualitativos que cuantificamos aquí: que LaTeX es de un solo hilo, que por tanto el rendimiento de un solo núcleo gobierna el tiempo de compilación, y que «más núcleos solo ayudarán si estás intentando compilar más documentos de los que tienes núcleos de CPU libres» \[1]. Luego da la regla lineal de dimensionamiento — una base de 2 núcleos/3 GiB más un núcleo y un gigabyte por cada cinco a diez usuarios concurrentes — que motivó este estudio. Nuestra contribución es convertir estas afirmaciones en leyes medidas (Ecuaciones (1) y (2)), y mostrar dónde se rompe la regla lineal: no tiene un término para la caché de páginas compartida que hace que el muro de memoria sea superlineal, ni un término para los dos parámetros de software que dominan el resultado.

### 7.2 Estudios de capacidad de compilación y CI

La medición de sistemas de compilación bajo concurrencia está bien establecida fuera del contexto de LaTeX. LightSys informa de que los sistemas CI convencionales que compilan dentro de contenedores Docker degradan su E/S a medida que aumenta la tasa de llegada de solicitudes de extracción, con un cuello de botella que aparece alrededor de once solicitudes concurrentes \[17]; TAOS-CI observa que la compilación domina el tiempo de reloj del CI, al representar el 60–67% de la duración total del pipeline en proyectos grandes \[18]. Nuestro sistema difiere en un aspecto que resulta ser decisivo: una compilación de LaTeX es interactiva. Un trabajo de CI que tarda el doble es un inconveniente; una compilación que tarda el doble la observa directamente un usuario que espera en un panel de vista previa, razón por la cual tratamos el tiempo de espera no como un umbral de fallo sino como un parámetro de capacidad.

### 7.3 Sobrecostes de contenedores

Trabajos recientes descomponen la latencia de arranque de los contenedores Docker a lo largo de los niveles de almacenamiento \[19] y caracterizan el rendimiento de los contenedores en el borde \[20]. En nuestro entorno, el arranque del contenedor por compilación se amortiza: es una pequeña constante en relación con una compilación de 9 s, y el tiempo de vuelo libre $$T\_1$$ que ajustamos lo absorbe. La propiedad del contenedor que sí importa es la *ausencia* de límites de recursos (§6.2), que convierte un exceso de memoria por compilación en un fallo de todo el host.

### 7.4 LaTeX como entrada no confiable

La compilación en sandbox existe porque TeX es un lenguaje de programación y los documentos son entrada no confiable \[21, 22]. Esa elección de diseño es lo que hace posible este estudio — cada compilación es un contenedor aislado con comportamiento de recursos observable — y también lo que hace que el límite de memoria ausente sea relevante, ya que los operadores que lo despliegan asumen esa aislamiento.

### 7.5 El compilador como objeto de estudio

TeX en sí está bien documentado como lenguaje \[16], pero su comportamiento como *objetivo de compilación* ha atraído atención solo recientemente. Tan y Rigger \[8] compilan un gran corpus de fuentes de arXiv a través de distintos motores y versiones de distribución y descubren que la elección del motor no es sustituible: solo una fracción de un porcentaje de los documentos produce salida idéntica byte a byte bajo XeTeX y pdfTeX. Ese resultado afecta directamente a nuestra metodología. La capacidad es una propiedad de un documento *y* de un motor, así que un benchmark que no fije ambos no es reproducible; por tanto, fijamos un documento, un motor y una distribución (`texlive-full:2025.1`) en todo momento, y reportamos el motor en cada pie de figura. También acota la generalidad de nuestros números de una forma que merece decirse claramente: caracterizan a XeLaTeX en este documento, no a TeX en abstracto.

El trabajo sobre sistemas de *compilación* está impulsado en gran medida por la práctica. El proyecto LaTeX3 `l3build` \[13] estandariza las pruebas de regresión y el empaquetado, y los benchmarks independientes comparan herramientas envoltorio — una encuesta de 26 sistemas de compilación encuentra que un preámbulo precompilado vale aproximadamente un 20% frente a una ejecución simple y un 40% frente a `latexmk` \[14]. Estos optimizan la compilación *individual* . Son ortogonales a lo que medimos y se combinan con ello: una caché de preámbulo acorta $$T\_1$$, y toda cifra de capacidad en este artículo escala con $$T\_1$$.

### 7.6 Control de concurrencia en el editor, no en el compilador

La mitad colaborativa de Overleaf se apoya en una línea de trabajo bien establecida. La transformación operativa se origina con Ellis y Gibbs \[9] y se hizo práctica para clientes con alta latencia gracias al sistema Jupiter \[10], cuyo diseño es reconocible en `document-updater`: un servidor que ordena operaciones y un búfer por documento contra el que los clientes se sincronizan. Los tipos de datos replicados sin conflicto \[11] resuelven el mismo problema sin un secuenciador central. Esta distinción es lo que hace que la topología de §4.3 funcione en absoluto: como el búfer de actualizaciones pendientes vive en Redis compartido en lugar de en la memoria de una instancia, una compilación enrutada a cualquier réplica observa las pulsaciones más recientes, y la afinidad de compilación puede elegirse por localidad de caché y no por corrección.

### 7.7 Modelos de capacidad

La ley de Amdahl \[24] acota la aceleración derivada del paralelismo y la ley de Little \[23] relaciona la ocupación con la tasa de llegada y el tiempo de servicio; ambas se usan arriba. La ley universal de escalabilidad de Gunther \[12] amplía la primera con un término retrógrado para el retraso de coherencia, prediciendo que el rendimiento alcanza un máximo y luego declina. Observamos que nuestro sistema *no* muestra ese régimen retrógrado hasta $$N=1024$$: el rendimiento se satura y la latencia crece, pero nada colapsa. La razón es estructural más que afortunada — las compilaciones no comparten ningún estado que mantener coherente, así que el término que añade la ley es casi cero, y la meseta de admisión de §4.3 limita la contención antes de que pueda importar.

## 8. Recomendaciones para los operadores

{% stepper %}
{% step %}

## Corrija los dos parámetros de software antes de comprar hardware

Ambos son gratis y ambos valen más que cualquier única mejora de hardware que hayamos medido. Suba `features.compileTimeout` a un valor que sus usuarios realmente toleren — el máximo que acepta CLSI es 600 s — y, si espera superar 65 compilaciones simultáneas, o bien eleve `compileConcurrencyLimit` en una imagen derivada o escale horizontalmente. No hacer ninguna de las dos cosas significa pagar por núcleos que el software se niega a usar.
{% endstep %}

{% step %}

## Dimensione una máquina por su punto de inflexión, no por su techo

El barrido del gran ejecutor (§4.3) separa dos números que suelen confundirse. *techo* — la mayor concurrencia que sigue devolviendo todos los PDF — es de al menos 1024 en un servidor de 64 núcleos, y nunca llegamos a él. El *punto de inflexión* — el punto a partir del cual la latencia de cola deja de crecer suavemente y empieza a duplicarse — está en 512, y el último punto de operación cómodo por debajo de él es 256. Entre $$N=256$$ y $$N=512$$ el $$p\_{95}$$ la espera pasa de dos minutos a casi seis; entre 512 y 1024 alcanza ocho. Un operador que dimensiona según el techo despliega un sistema que técnicamente funciona y que nadie quiere usar.

Para esta máquina y este documento, el punto operativo recomendado es, por tanto, **256 compilaciones concurrentes**, que es $$4\times$$ el número físico de núcleos y $$2\times$$ el número de hilos, y que se mantiene $$p\_{95}$$ cerca de 120 s. Sugerimos fijar `compileConcurrencyLimit` a ese valor en lugar de dejarlo alto: admitir 1024 compilaciones a la vez hace que todos esperen ocho minutos, mientras que admitir 256 y encolar el resto sirve a la mayoría de los usuarios en dos. La cola perjudica a los que llegan tarde; la contención perjudica a todos.
{% endstep %}

{% step %}

## Trate estos como números de peor caso

Cada nivel de la Tabla 2 es una compilación en frío lanzada simultáneamente. Ninguna de esas condiciones se cumple en producción: una compilación en caliente del mismo documento tarda 8.6 s frente a 28.3 s en frío, un factor de $$3.3$$, y los usuarios reales no pulsan el botón en el mismo segundo. Una población en estado estacionario que recompila cada dos minutos con una tasa típica de aciertos de caché podrá sostener, por tanto, bastantes más escritores de los que sugiere por sí solo el número de concurrencia — del orden de mil o más autores activos en el punto operativo 256. La cifra de concurrencia es un límite sobre el estallido instantáneo, no un recuento de asientos.
{% endstep %}

{% step %}

## Decida primero el presupuesto de latencia y luego deduzca el tamaño

La ecuación (1) se invierte directamente. Para una espera objetivo $$T$$ a reloj $$f$$ en $$C$$ núcleos, la concurrencia que cabe es $$N \le C,fT/k$$ con $$k\approx28 GHz·s$$ para este documento. Un presupuesto de 60 s en 8 núcleos a 3 GHz da $$N\le51$$; un presupuesto de 120 s lo duplica. Publicar el presupuesto junto con la capacidad es la única forma honesta de expresar cualquiera de las dos cosas.
{% endstep %}

{% step %}

## Compre primero memoria, luego núcleos, y compruebe en qué muro está

Por debajo de 32 GiB medimos casi ningún beneficio de núcleos adicionales. El diagnóstico es barato: si los fallos aparecen como HTTP 502 con el huésped falto de memoria, añada memoria; si aparecen como `agotado` con memoria de sobra, añada núcleos o aumente el tiempo de espera. Los operadores pueden leer esto a partir de la misma firma de fallo que usamos para clasificar configuraciones.
{% endstep %}

{% step %}

## Prefiera el reloj para la experiencia, los núcleos para la población

Porque $$T\propto 1/f$$ se mantiene sin curvatura (2% entre 1.0–5.5 GHz), un reloj más rápido hace que cada compilación sea más rápida para cada usuario. Más núcleos no hacen más rápida ninguna compilación individual; solo admiten más compilaciones concurrentes. Las implantaciones cuya queja sea «las compilaciones son lentas» deberían comprar reloj; las cuya queja sea «las compilaciones fallan al llegar la fecha límite» deberían comprar memoria y núcleos.
{% endstep %}

{% step %}

## Escale horizontalmente en lugar de verticalmente más allá del techo

Más allá de 65 compilaciones concurrentes, la vía soportada es el escalado horizontal (Figuras 2c y 10, desarrolladas en §9): múltiples instancias de aplicación detrás de un balanceador de carga con afinidad de sesión por cookie, compartiendo MongoDB central, Redis y almacenamiento compatible con S3, con `git-bridge` dejado como singleton.
{% endstep %}

{% step %}

## No confíe en la aislación por compilación

Hasta que se corrija el límite de memoria del ejecutor de Docker (§6.2), un único documento patológico puede agotar el host en lugar de ser matado por sí solo. Los operadores que necesiten esa garantía deberían imponerla ellos mismos en lugar de esperar a que ocurra. El mecanismo que usamos en el host grande es una slice de systemd con un techo duro, a la que luego se apunta el demonio de Docker para que cada contenedor que cree quede contabilizado dentro de ella:

```ini
[Slice]
MemoryMax=940G
```

Un detalle aquí cuesta una tarde si se pasa por alto. Una slice llamada `docker-capped.slice` no se sitúa junto a `docker.slice`; se sitúa *dentro* de ella, porque el guion es el separador de jerarquía y no parte del nombre. Un techo que parece no tener efecto suele haberse aplicado un nivel por encima de donde viven realmente los contenedores. Verifíquelo leyendo el pico de nuevo desde `memory.max_usage_in_bytes` después de una ejecución en lugar de confiar en el archivo de configuración — en nuestro host, el cgroup de compilación nunca superó una quinta parte de su techo ni siquiera con 1024 compilaciones simultáneas, lo que en sí mismo es la evidencia de que el daemon y no la memoria era la restricción vinculante.
{% endstep %}
{% endstepper %}

<figure><img src="/files/6f38cb257d7333f7c2a61229bf2773e0bcce7381" alt=""><figcaption></figcaption></figure>

**Figura 10.** Topología de referencia para un despliegue escalado horizontalmente, trazada a partir de la configuración que verificamos. Las réplicas de aplicación son intercambiables y no retienen nada duradero, así que pueden añadirse y eliminarse libremente. Tres componentes no lo son: Redis, cuyo búfer de documentos es lo que permite que una compilación enrutada a cualquier réplica vea las pulsaciones más recientes; el almacén de objetos, que pasa de ser opcional a ser obligatorio con más de una réplica; y `git-bridge`que mantiene los repositorios en disco local sin ruta de replicación y debe ejecutarse como singleton junto a una réplica designada.

## 9. Un despliegue multimaquina de referencia

Todo lo anterior mide una sola máquina. Esta sección describe la forma distribuida con suficiente detalle como para construirla y — porque la pregunta a la que realmente se enfrenta un operador no es *cómo* sino *si* — establece primero el punto en el que merece la pena el esfuerzo.

### 9.1 Cuándo está justificada la forma distribuida

Una sola máquina es más barata de operar en todos los aspectos que importan: un dominio de fallo, ningún estado compartido que mantener coherente, ningún enrutamiento que salir mal. Nuestros datos ponen tres umbrales para saber cuándo abandonarla.

#### 9.1.1 Por debajo de 65 compilaciones concurrentes, no

El techo por instancia es una constante de software, no de hardware (§6.1). Hasta que la carga ofrecida se le acerque, una segunda máquina añade modos de fallo y no compra nada. El host de 64 núcleos sirvió 256 compilaciones concurrentes con éxito total solo después de que `compileConcurrencyLimit` se elevó; un operador que todavía no ha cambiado ese único valor no está limitado por el hardware y no debería estar comprando hardware.

#### 9.1.2 Entre 65 y aproximadamente 500, escale primero hacia arriba

El escalado vertical siguió siendo lineal en todo nuestro rango y nunca entró en un régimen retrógrado. Un host grande alcanzó 1024 compilaciones frías simultáneas con un 100% de éxito (§4.3); el punto de inflexión en latencia apareció en 512, no antes. Dentro de esa banda, una máquina más grande es estrictamente más simple que varias más pequeñas y, según §4.4, una más rápida mejora la experiencia de cada usuario en lugar de solo admitir a más de ellos.

#### 9.1.3 Vaya distribuido por disponibilidad, no por rendimiento

La razón honesta para ejecutar más de una réplica de aplicación por debajo del techo es que una máquina es una fuente de alimentación, un kernel y una ventana de actualización. Esa es una razón legítima y es la que daríamos; simplemente no es un argumento de capacidad, y confundir ambos lleva a los operadores a comprar réplicas cuando lo que necesitaban era memoria.

### 9.2 Niveles y su dimensionamiento

La Figura 10 muestra la topología. Tiene cuatro niveles, y escalan sobre cantidades diferentes — que es precisamente el punto de separarlos.

#### 9.2.1 Borde

Un balanceador de carga, o dos por disponibilidad. Termina TLS y no hace nada costoso; escala con el número de conexiones, no con el número de compilaciones, y una instancia pequeña basta para las cargas estudiadas aquí. Lo que importa es su configuración, no su tamaño (§9.3).

#### 9.2.2 Réplicas de aplicación

Estas soportan la carga de compilación y son el único nivel que escala con la concurrencia. Dimensione cada una siguiendo las reglas de §8 — memoria antes que núcleos, luego reloj — y después fije el número de réplicas para cubrir la concurrencia máxima dividida por el techo por réplica. Las réplicas no retienen nada duradero: su disco local guarda restos de compilación y una caché de salida, ambos reconstruibles. Esto es lo que las hace seguras para añadir y retirar libremente, y conviene verificarlo en lugar de asumirlo, porque una sola ruta de `filestore` mal configurada convierte silenciosamente el nivel en uno con estado.

#### 9.2.3 Estado

Redis, MongoDB y un almacén de objetos compatible con S3, en hosts separados. Redis es el de carga estructural y el menos obvio: contiene el almacén de sesiones y el búfer de documento en vivo, que es lo que permite que una compilación enrutada a cualquier réplica observe pulsaciones tecleadas contra una réplica distinta. Un operador que trate Redis como una caché y lo dimensione para expulsión producirá compilaciones de documentos obsoletos que son extremadamente difíciles de diagnosticar, porque nada falla — la salida simplemente es incorrecta. MongoDB escala con el número de proyectos y no con la tasa de compilación. El almacén de objetos es opcional con una réplica y obligatorio más allá de ella.

#### 9.2.4 El singleton

`git-bridge` mantiene los repositorios en disco local, conserva un índice local y no tiene ruta de replicación. Debe ejecutarse como exactamente una instancia, fijada junto a una réplica designada, y es el componente que hace que el despliegue sea casi sin estado. Planifique su host en consecuencia: su disco es el que necesita copias de seguridad.

| **Nivel**            | **Cantidad**         | **Escala con**                     |
| -------------------- | -------------------- | ---------------------------------- |
| Balanceador de carga | 1–2                  | conexiones concurrentes            |
| Aplicación           | $$\lceil N/C\rceil$$ | compilaciones concurrentes máximas |
| Redis                | 1 (+réplica)         | sesiones de edición activas        |
| MongoDB              | 1 (+réplica)         | proyectos almacenados              |
| Almacén de objetos   | 1 clúster            | bytes totales del proyecto         |
| `git-bridge`         | 1 exactamente        | repositorios en disco              |

**Tabla 4.** Niveles de referencia. Solo el nivel de aplicación escala con la concurrencia; su dimensionamiento es el tema de §8.

### 9.3 Enrutamiento: es la parte fácil de hacer mal

Tres clases de solicitudes deben llegar a tres lugares distintos, y la configuración por defecto de una sola regla satisface como mucho a dos de ellas.

El tráfico de compilación bajo `/project/` debe distribuirse mediante hash consistente sobre el identificador del proyecto, para que la caché de compilación de un proyecto se quede con una sola réplica. Usamos de HAProxy `balance hash path,field(3,/)` con `hash-type consistent` y `hash-balance-factor 150`. La elección importa al escalar: con afinidad por cookie, las sesiones existentes quedan fijadas indefinidamente a su réplica original y una réplica recién añadida recibe solo usuarios nuevos, de modo que la máquina por la que el operador acaba de pagar no absorbe ninguna de las cargas que motivaron su compra. El hash consistente redistribuyó el 35% de los proyectos al escalar en nuestra configuración, frente al 0% de las cookies.

El tráfico de sesión es distinto. Cuando la actualización de WebSocket falla y `socket.io` recurre a sondeo XHR, los sondeos sucesivos de una sesión deben llegar a una sola réplica, y no hay ningún identificador de proyecto en la ruta para hacer hash. Este tráfico necesita un backend separado con afinidad por cookie. Diseñamos esta división, pero no la desplegamos; la señalamos como una laguna en lugar de afirmarla.

Por último, `/git/` debe llegar a la réplica junto a la cual `git-bridge` se ejecuta. Se enruta a esa réplica y no a `git-bridge` directamente porque el puente autentica sus callbacks contra los puntos finales OAuth de la aplicación y resuelve las URL de blobs a través de ella; saltarse la réplica rompe la autenticación en lugar de mejorar nada.

### 9.4 La reducción de escala necesita un búfer de drenaje

Eliminar una réplica no es simétrico a añadir una: una compilación en vuelo se pierde y el usuario ve un fallo que no causó. La secuencia que funciona es detener primero el tráfico nuevo, esperar y solo entonces terminar. Lo implementamos como un gancho pre-stop que retiene el pod durante un intervalo configurable mientras el balanceador marca el backend como drenando — lo bastante corto como para probarlo en minutos, y en producción lo bastante largo para el final natural de una sesión, horas más que segundos. El intervalo es la perilla de ajuste que decide si la elasticidad es invisible o exasperante.

Una restricción más que hallamos por medición y no por diseño: el autoescalado basado en CPU no funciona para esta carga de trabajo. La utilización propia del pod de aplicación se situó en 22 m core frente a un total de nodo de 3997 m core, porque el trabajo de compilación ocurre en contenedores hermanos que el pod no contabiliza. Cualquier señal que se use para escalar este nivel debe contar los contenedores de compilación en ejecución, no la CPU del pod.

## 10. Implicaciones más allá de Overleaf

Nada en §4.2 o §4.3 es específico del código de Overleaf. Las leyes medidas se derivan de tres propiedades que comparte cualquier servicio de LaTeX alojado: la unidad de trabajo es un proceso de un solo hilo, está aislado en un contenedor, y su conjunto de trabajo es un gran árbol de solo lectura que la caché de páginas debe mantener. Tres consecuencias se trasladan directamente a cualquiera que construya un servicio así.

### 10.1 Proporcione memoria, luego núcleos

El resultado más fuerte de la matriz es negativo: por debajo de 16 GiB el número de núcleos es casi irrelevante, y solo a 48 GiB las configuraciones de 4, 8 y 16 vCPU se separan de verdad (143, 268, 331). Un operador que lea la regla convencional como «añada un núcleo por cada cinco usuarios» compra el recurso equivocado. El mecanismo es la caché de páginas compartida sobre el árbol de distribución, y es una propiedad del tamaño de TeX Live más que de cualquier front-end en particular.

### 10.2 La tasa de admisión es un recurso y suele olvidarse

En $$N=1024$$ nuestro servidor nunca mantuvo más de 205 sandboxes vivos (Figura 7b) aunque cada solicitud llegó a la vez. La creación de contenedores, no la compilación, fue el factor limitante — coherente con estudios de medición que atribuyen el coste de arranque de contenedores a la sobrecarga de ejecución y no al tamaño de la imagen \[19, 20]. Un servicio que dimensiona solo CPU y memoria encontrará que su comportamiento en ráfagas está gobernado por una cantidad que nunca midió. La forma práctica de esto es la recomendación de §8: limite la admisión deliberadamente, porque una cola que usted elige es mejor que una cola que descubre.

### 10.3 Un sandbox que sobrevive a su compilación invalida el modelo

Cada cifra de capacidad aquí asume que el contenedor se crea, realiza una compilación y sale — una vida útil de decenas de segundos y un ciclo de trabajo cercano a uno solo mientras se ejecuta. Dos patrones de diseño recientes rompen esa suposición, y la rompen de la misma manera.

El primero es el sandbox persistente por usuario. Asignar a cada usuario un entorno privado fijo convierte un conjunto multiplexado estadísticamente en un conjunto de reservas: un servicio que podría servir 256 compilaciones concurrentes desde 64 núcleos mediante reparto temporal solo puede servir 16 usuarios si a cada uno se le asignan cuatro núcleos dedicados, un orden de magnitud menos para el mismo hardware. Nuestros datos cuantifican el coste de esa elección en lugar de argumentar en contra — las reservas compran previsibilidad, y el tipo de cambio es aproximadamente $$16\times$$ en el punto operativo que recomendamos.

El segundo, y más reciente, es el agente de IA que comparte el sandbox con el compilador. En plataformas de autoría asistida por agentes, el mismo contenedor que ejecuta XeLaTeX puede alojar también un agente de programación de larga duración, de modo que está ocupado continuamente en lugar de hacerlo en ráfagas. Los practicantes informan exactamente del síntoma que el modelo predice para tales despliegues — lentitud sostenida con recuentos modestos de usuarios \[15]. Merece la pena enunciar con precisión la interacción, porque no es simplemente «más carga». Tres de nuestros hallazgos se combinan. La ocupación deja de ser en ráfagas, así que la ley de reparto temporal de §4.2 se aplica a toda la población a la vez en lugar de a la fracción que está compilando en ese momento. La caché de páginas, que es lo que aporta el rendimiento de memoria superlineal de §4.1, ahora se comparte con el propio conjunto de trabajo del agente y deja de estar caliente para TeX. Y el límite de memoria del contenedor ausente de §6.2 se vuelve mucho más peligroso, porque un contenedor que nunca sale nunca devuelve su memoria.

No medimos una plataforma así ni hacemos ninguna afirmación sobre ningún producto específico. Lo que sí podemos decir es lo que nuestros números implican para el diseño: una arquitectura que da a cada usuario un sandbox multinúcleo de larga duración debería dimensionarse como un sistema de reservas, no por las cifras de concurrencia informadas aquí, y la capacidad que puede esperar es más cercana a su número de núcleos dividido por núcleos por usuario que a cualquier cosa de la Tabla 1.

## 11. Amenazas a la validez

### 11.1 Un solo documento

Todas las mediciones usan un documento XeLaTeX de 63 páginas. Las capacidades absolutas diferirán para otros documentos; las leyes de escalado, que son razones, no deberían hacerlo. Un documento con un conjunto residente sustancialmente mayor movería el muro de memoria sin cambiar su carácter superlineal.

### 11.2 Host virtualizado

Los huéspedes se ejecutan bajo KVM en una sola máquina física, así que las cifras absolutas incluyen sobrecarga de virtualización y los huéspedes comparten una caché de páginas del host y un dispositivo NVMe. Mitigamos el mayor factor de confusión apagando los huéspedes no relacionados tras observar que la presión de memoria del host infla los promedios de carga dentro de la máquina invitada en más de $$3\times$$ a concurrencia idéntica.

### 11.3 Llegada simultánea

Cada compilación se lanza en un instante, que es el peor caso. Los usuarios reales llegan como un proceso estocástico, así que un despliegue dimensionado con nuestros números tiene margen y no déficit — pero el pico al final de una fecha límite de entrega está más cerca de nuestro modelo que de uno Poisson.

### 11.4 Configuraciones de borde

A 2 GiB, el sistema está lo bastante cerca del colapso como para que ejecuciones repetidas de la misma configuración puedan diferir en una compilación. Informamos el valor conservador y no sacamos conclusiones de diferencias de $$\pm 1$$ en ese régimen.

## 12. Disponibilidad

El sistema bajo prueba, las herramientas de despliegue y el proyecto aguas arriba del que deriva son todos públicos:

* Ayakaleaf Pro — <https://github.com/ayaka-notes/ayakaleaf-pro>
* Kit de despliegue — <https://github.com/ayaka-notes/toolkit>
* Documentación — [https://ayakaleaf-pro.ayaka.space](https://ayakaleaf-pro.ayaka.space/)
* Overleaf aguas arriba — <https://github.com/overleaf/overleaf>
* Imágenes de compilación de TeX Live — `ghcr.io/ayaka-notes/texlive-full:2025.1`

Cada ubicación fuente que citamos se da como una ruta relativa al repositorio con un número de línea contra Ayakaleaf Pro v6.2.2, y los dos commits aguas arriba que fechamos (`9a519f0d3d`, `5d472e9b38`) son accesibles en el historial de Overleaf.

## 13. Contribuciones

Musicminion diseñó el estudio, proporcionó y operó los bancos de pruebas, dirigió la línea de investigación y verificó cada medición informada aquí. Claude Opus 5 (Anthropic) construyó y operó el arnés de benchmarks, automatizó los despliegues, realizó la arqueología del código fuente, produjo las figuras y redactó el manuscrito. Ambos autores revisaron el texto final. Cuando una ejecución se informa como contaminada — el barrido de 1021 sesiones de §4.3 y el nivel anómalo de §4.1 — el defecto se encontró durante la revisión y la ejecución se repitió antes de la publicación en lugar de descartarse silenciosamente. $$N=256$$ nivel de §4.1 — el defecto se encontró durante la revisión y la ejecución se repitió antes de la publicación en lugar de descartarse silenciosamente.

Los lectores deben tener en cuenta que las políticas de autoría de ACM, IEEE y la ICMJE actualmente reservan la autoría para las partes capaces de asumir responsabilidad por un trabajo, y exigirían que la contribución del segundo autor se registrara como una declaración y no como una firma. Aquí declaramos explícitamente la división del trabajo para que el registro sea exacto bajo cualquiera de las dos convenciones.

## 14. Conclusión

La planificación de capacidad para Overleaf autohospedado no consiste en escalar un solo recurso. Tres hallazgos deberían cambiar cómo se hace.

Primero, por debajo de 32 GiB de memoria de huésped, el número de núcleos apenas importa: a 16 GiB, las capacidades de los huéspedes de 4, 8 y 16 vCPU difieren en menos de un 8%. La memoria, a través de la caché de páginas compartida sobre el árbol de TeX Live, fija el límite; los núcleos solo empiezan a importar una vez que la memoria es generosa.

Segundo, dos parámetros de software pesan más que el hardware. Levantar el techo codificado de 65 compilaciones de CLSI y aumentar el tiempo de espera de compilación por defecto de 180 s llevó a un huésped de 8 vCPU / 48 GiB de 64 a 268 compilaciones concurrentes — un factor de 4.2 sin hardware adicional. Ninguno es descubrible a partir de la documentación de configuración; uno no es configurable en absoluto.

En tercer lugar, la pregunta «¿cuántos usuarios concurrentes admite esta máquina?» está insuficientemente especificada. La concurrencia en este sistema es pura compartición de tiempo, y la capacidad es la que admita el tiempo de espera. La forma honesta de la respuesta establece ambas cosas: *esta máquina sirve* $$N$$ *compilaciones simultáneas si los usuarios esperan* $$T$$ *segundos*, con $$N$$ y $$T$$ relacionados por la Ecuación (1).

También informamos de un defecto latente: el límite de memoria por contenedor en el ejecutor de Docker ha sido ineficaz desde 2018, tanto en magnitud como en ubicación. Su efecto práctico es que el agotamiento de memoria en un despliegue pequeño derriba todo el servicio en lugar de la compilación responsable.

## Referencias

\[1] Overleaf. *Requisitos de hardware*, Documentación on-premises. <https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements>

\[2] Overleaf. *Escalado horizontal*, Documentación on-premises. <https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling>

\[3] Overleaf. *Microservicios*, Documentación on-premises. <https://docs.overleaf.com/on-premises/getting-started/microservices>

\[4] Overleaf. *Repositorio de código fuente*. <https://github.com/overleaf/overleaf>

\[5] Ayaka-notes. *Ayakaleaf Pro*. <https://github.com/ayaka-notes/ayakaleaf-pro>

\[6] Ayaka-notes. *Kit de herramientas de Overleaf*. <https://github.com/ayaka-notes/toolkit>

\[7] D. Karger, E. Lehman, T. Leighton, R. Panigrahy, M. Levine y D. Lewin. *Hashing consistente y árboles aleatorios: Protocolos de caché distribuida para aliviar los puntos calientes en la World Wide Web*. STOC, 1997.

\[8] J. Tan y M. Rigger. *Inconsistencias en documentos producidos con TeX*. En *Actas del 33.º Simposio Internacional ACM SIGSOFT sobre Pruebas y Análisis de Software (ISSTA)*, Viena, 2024. doi: <https://doi.org/10.1145/3650212.3680370>

\[9] C. A. Ellis y S. J. Gibbs. *Control de concurrencia en sistemas de trabajo en grupo*. En *Actas de ACM SIGMOD*, pp. 399–407, 1989.

\[10] D. A. Nichols, P. Curtis, M. Dixon y J. Lamping. *Ventaneo de alta latencia y bajo ancho de banda en el sistema de colaboración Jupiter*. En *Actas de ACM UIST*, pp. 111–120, 1995.

\[11] M. Shapiro, N. Preguiça, C. Baquero y M. Zawirski. *Tipos de datos replicados sin conflictos*. En *Actas de SSS*, pp. 386–400, 2011.

\[12] N. J. Gunther. *Planificación de capacidad guerrilla: un enfoque táctico para planificar aplicaciones y servicios altamente escalables*. Springer, 2007.

\[13] Proyecto LaTeX3. *l3build — Un sistema de pruebas y construcción para (La)TeX*. CTAN.

\[14] M. Isaksson. *¿Qué sistema de compilación LaTeX es el más rápido? Un benchmark*. <https://blog.martisak.se/latex-build-systems-comparison/>

\[15] Informes de profesionales sobre la latencia sostenida en plataformas de autoría asistida por agentes que coubican un agente de codificación persistente con el compilador LaTeX en un entorno aislado por usuario. Lo citamos como experiencia operativa informada, no como una medición controlada; no realizamos un benchmark de una plataforma así.

\[16] D. E. Knuth. *El libro de TeX*. Addison-Wesley, 1984.

\[17] G. Lim, M. Ham, J. Moon y W. Song. *LightSys: Sistema CI ligero y eficiente para mejorar la velocidad de integración del software*. arXiv:2101.07961 \[cs.SE], 2021. Preimpresión.

\[18] G. Lim, M. Ham, J. Moon, W. Song, S. Woo y S. Oh. *TAOS-CI: Sistema de integración continua ligero y modular para computación en el borde*. arXiv:2101.08889 \[cs.SE], 2021. Preimpresión.

\[19] S. Khan. *Descomposición del rendimiento de arranque de contenedores Docker: un estudio de medición en tres niveles sobre infraestructura heterogénea*. arXiv:2602.15214, 2026. Preimpresión.

\[20] R. Gupta y K. Nahrstedt. *Caracterización del rendimiento de los contenedores en la computación en el borde*. arXiv:2505.02082, 2025. Preimpresión.

\[21] S. Checkoway, H. Shacham y E. Rescorla. *¿Son seguros los formatos de datos solo de texto? O bien, use este archivo de clase LaTeX para tomar el control de su computadora*. En *Actas del Taller USENIX sobre Explotaciones a Gran Escala y Amenazas Emergentes (LEET)*, 2010.

\[22] G. Lacombe, K. Masalygina, A. Tahiri, C. Adam y C. Lauradoux. *¿Puede aceptar archivos LaTeX de desconocidos? Diez años después*. arXiv:2102.00856 \[cs.CR], 2021. Preimpresión.

\[23] J. D. C. Little. *Una prueba de la fórmula de colas* $$L=\lambda W$$. Operations Research, 9(3):383–387, 1961.

\[24] G. M. Amdahl. *Validez del enfoque de procesador único para lograr capacidades de computación a gran escala*. AFIPS, 1967.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ayakaleaf-pro.ayaka.space/blog/es/2026/overleaf-benchmark.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
