Escalado y alta disponibilidad
MCP REST Bridge corre feliz como un único proceso — una instancia Bun con un fichero SQLite local maneja mucho. Cuando necesitas redundancia o más throughput, escala horizontalmente: ejecuta varias instancias idénticas tras un load balancer, coordinadas a través de una base de datos SQLite compartida.
El modelo
- Manejo de requests stateless. Cada llamada de tool es autocontenida; cualquier instancia puede servir cualquier llamada REST.
- SQLite es la capa de coordinación. Config admin, guards, keys, audit, uso y las primitivas HA viven todas en la base de datos. Apunta cada instancia al mismo
DB_PATH(almacenamiento compartido / un volumen compartido) para que vean una sola config. - Flags de HA opt-in activan comportamiento cross-instancia (abajo) — están off por defecto para que un único nodo se mantenga simple.
Activar las primitivas HA
| Setting | Efecto |
|---|---|
RATE_LIMIT_SHARED=true | Los rate limits usan contadores fixed-window en SQLite, de modo que un límite por tool se aplica en todas las instancias, no por proceso. |
REGISTRY_SYNC=true | Cada instancia reconcilia periódicamente su registry en vivo desde SQLite — un cliente registrado (o eliminado) en un nodo se propaga a los otros. |
Los loops en background que deben correr una vez — evaluación de alertas, schedules de mantenimiento y el loop de health-check/auto-eliminación — eligen un único líder automáticamente vía un lease en SQLite. Esto no es una flag; siempre está activo y no requiere configuración.
Load balancing de tus backends
Separado de escalar el bridge mismo, un único cliente puede fan-out entre varios targets de backend (load balancing N-way), configurado por cliente desde la admin API. Un target que falla se salta durante LB_TARGET_COOLDOWN_MS (por defecto 30s) antes de probarlo de nuevo. Combina con canary/failover por cliente (consulta Guardrails y resiliencia) para degradación con gracia.
Sesiones MCP y sticky routing
El transporte Streamable HTTP mantiene estado por sesión en memoria en la instancia que abrió la sesión. Dos opciones:
- Sticky sessions — habilita afinidad de sesión en tu load balancer para los endpoints MCP (
/mcp,/mcp/:name,/mcp-custom/:bundle) para que una sesión se quede en una instancia. Recomendado para clientes streaming. - Llamadas stateless — los clientes que abren un request fresco por llamada no necesitan afinidad y se balancean libremente.
El proxy REST y la admin API no necesitan afinidad.
Caveats que debes conocer
SQLite compartido requiere almacenamiento compartido. SQLite sobre un filesystem de red tiene quirks de locking; prefiere un volumen que todas las instancias monten localmente, o mantén las escrituras modestas. Para volumen de escritura muy alto, ejecuta menos instancias, más grandes.
Todas las guardas salvo el rate limiter cuentan por instancia.
RATE_LIMIT_SHARED=truemueve los contadores de rate limit a SQLite; nada más se mueve. Los circuit breakers, la caché de respuestas, el coalescing de peticiones en vuelo y los cooldowns de destinos del balanceador viven en la memoria del proceso que vio el tráfico. Con N instancias, en concreto:Guarda Efecto con N instancias Circuit breaker Un backend caído tiene que fallar failureThresholdveces por instancia antes de que abran todasCaché de respuestas Hasta N copias de la misma entrada; el hit rate baja aproximadamente N veces con tráfico repartido Coalescing Colapsa duplicados concurrentes dentro de una instancia, no entre ellas Cooldown de destino Un destino marcado como caído en una instancia lo siguen intentando las demás hasta que también fallan Ninguno es un bug de corrección: cada instancia llega a la misma decisión, solo que por su cuenta y con sus propias evidencias. Dimensiona
CIRCUIT_BREAKER_FAILURE_THRESHOLDy los TTL de caché contando con el número de réplicas, y usa sticky sessions (arriba) si el hit rate de caché te importa.La cadena de hash del audit es por instancia. Su tamper-evidence (
verifyAuditChain) asume un único escritor; la integridad de la cadena cross-instancia está fuera de scope — streamea a un SIEM (AUDIT_SINK_URL) para un registro consolidado y ordenado en su lugar.Un health-eviction hold también es por instancia, si enrutas con
/livezen vez de/readyz. Bajo la topología por defecto (solo-líder, gateada por/readyz) esto no importa — solo el líder llega a probar backends o servir tráfico. Pero si cambias el readiness a/livezpara que cada réplica sirva (consulta el override del readiness-probe del Helm chart →), un re-registro de cliente puede aterrizar en una réplica que no es el líder y solo limpiar la marca de eviction en memoria de esa réplica, dejando la propia marca del líder (y por tanto sureconcileFromDb()) atascada reteniendo el cliente. Vuelve a registrar contra el líder, o usaforgetClient/re-regístralo a través de una sesión anclada a él, para limpiar el hold.
Checklist
- [ ] Todas las instancias comparten un
DB_PATH - [ ]
RATE_LIMIT_SHARED=trueyREGISTRY_SYNC=true - [ ] El load balancer chequea
/health(o/livez) para enrutar tráfico REST/MCP del plano de datos — el manejo de requests es stateless, así que cualquier instancia sirve sin importar el liderazgo./readyzda 200 solo en el líder actual (leader lease + base de datos); apuntar ahí un LB de escalado por throughput saca de rotación a todas las demás instancias. Reserva el enrutado condicionado a/readyzpara una topología deliberada de failover activo/pasivo, y usa/livezpara el probe de liveness/reinicio del orquestador - [ ] Sticky sessions para los endpoints MCP
/mcp·/mcp/:name·/mcp-custom/:bundle(si usas streaming) - [ ]
AUDIT_SINK_URLconfigurado para un audit trail consolidado
Consulta Despliegue → para el setup del contenedor y Configuración → para cada flag.