Failover multirregión: qué falla de verdad en producción
El failover multirregión parece sencillo en un diagrama: la realidad es retraso de replicación, escrituras split-brain y cachés DNS poco cooperativas.

En la página de marketing de cualquier proveedor, el failover multirregión suena tan simple como accionar un interruptor: la primaria cae, la réplica toma el control, el tráfico se redirige y nadie se entera. Ya pasé por cuatro failovers reales —dos planificados, dos no— y ninguno se pareció a esa descripción. La distancia entre "tenemos una réplica en otra región" y "podemos hacer failover de verdad sin perder datos ni servir lecturas obsoletas" es donde vive la mayor parte del trabajo de ingeniería real.
Este artículo repasa los modos de falla que no aparecen hasta que ya estás en medio de un incidente, y las decisiones de diseño que determinan si el failover pasa desapercibido o se convierte en una interrupción de varias horas.
El retraso de replicación no es un error de redondeo
El retraso de replicación entre regiones suele tratarse como una métrica para vigilar, no como una restricción de diseño. Eso está al revés. El retraso no es solo "cuánto atraso lleva la réplica": es "cuántos datos perderemos si promovemos ahora mismo".
-- Check replication lag on a PostgreSQL streaming replica
SELECT
client_addr,
state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS bytes_behind,
extract(epoch FROM (now() - reply_time)) AS seconds_behind
FROM pg_stat_replication;Una réplica que va 200 ms por detrás en condiciones normales puede acumular más de 30 segundos de atraso durante un pico de tráfico o una escritura masiva por lotes, justo las condiciones bajo las cuales una primaria tiene más probabilidades de fallar. Si tu runbook de failover dice "promover la réplica", también debe decir "y así de muchos segundos de escrituras estamos dispuestos a perder para lograrlo".
// ❌ Blind promotion — no visibility into data loss
async function failover(replicaId: string): Promise<void> {
await promoteReplica(replicaId);
await updateDnsRecord(replicaId);
}
// ✅ Bounded promotion — refuses to fail over past a data-loss threshold
async function failover(
replicaId: string,
maxAcceptableLagSeconds: number,
): Promise<FailoverResult> {
const lag = await getReplicationLag(replicaId);
if (lag.seconds > maxAcceptableLagSeconds) {
return {
status: "blocked",
reason: `Lag ${lag.seconds}s exceeds threshold ${maxAcceptableLagSeconds}s`,
estimatedDataLoss: lag.approximateWriteCount,
};
}
await promoteReplica(replicaId);
await updateDnsRecord(replicaId);
return { status: "completed", lagAtPromotion: lag.seconds };
}El umbral en sí es una decisión de negocio, no de ingeniería. Consigue que el equipo de producto y el liderazgo aprueben una ventana aceptable de pérdida de datos antes del incidente, no durante él.
Split-Brain: dos primarias son peor que ninguna
El escenario del que nadie quiere hablar: la primaria "caída" en realidad no está muerta, solo es inalcanzable para tu verificador de salud. Mientras tanto, ya promoviste la réplica, y ahora ambas bases de datos aceptan escrituras de forma independiente. Este es el modo de falla más destructivo en configuraciones multirregión, y lo provocan particiones de red, no fallas de hardware.
// ❌ Health check with no fencing — promotes on a single failed probe
async function checkPrimaryHealth(): Promise<boolean> {
try {
await pingDatabase(PRIMARY_HOST, { timeout: 2000 });
return true;
} catch {
return false; // triggers immediate promotion elsewhere
}
}
// ✅ Fencing before promotion — actively prevents the old primary from writing
async function promoteWithFencing(
candidateReplica: string,
oldPrimary: string,
): Promise<void> {
// 1. Confirmed multiple independent probes agree primary is down
const consensus = await getQuorumHealthCheck(oldPrimary, { probes: 3 });
if (consensus.healthy) throw new Error("Primary reachable — abort promotion");
// 2. Fence the old primary at the network or storage layer BEFORE promoting
await revokeWriteAccess(oldPrimary); // e.g. STONITH, IAM policy, VPC rule
// 3. Only now is it safe to promote
await promoteReplica(candidateReplica);
}El fencing —cortar activamente la capacidad de escritura de la primaria anterior, en lugar de simplemente asumir que está muerta— es la parte que los equipos se saltan porque resulta operativamente molesta. También es la parte que evita el peor desenlace: dos "fuentes de verdad" que divergieron y ya no se pueden reconciliar automáticamente.
El DNS y los pools de conexiones no hacen failover tan rápido como crees
Incluso una promoción limpia y con fencing correcto no sirve de nada si la capa de aplicación sigue hablando con la primaria anterior. Dos culpables aparecen siempre:
- El cacheo de TTL de DNS — los clientes y los resolvers intermedios retienen la IP anterior mucho más allá del TTL configurado, sobre todo en plataformas administradas con cacheo de resolver agresivo.
- Los pools de conexiones — un pool que ya abrió 50 conexiones hacia la primaria anterior las seguirá usando tranquilamente hasta que fallen, no hasta que cambie el DNS.
// ❌ Pool holds stale connections indefinitely after failover
const pool = new Pool({
host: "db-primary.internal",
max: 50,
});
// ✅ Pool with bounded connection lifetime forces periodic re-resolution
const pool = new Pool({
host: "db-primary.internal",
max: 50,
maxLifetimeSeconds: 300, // connections recycle every 5 minutes
connectionTimeoutMillis: 3000,
});
pool.on("error", async (err) => {
if (isConnectionRefused(err)) {
await pool.end(); // force full reconnect, re-resolve DNS
}
});Los TTL de DNS bajos ayudan, pero no bastan por sí solos. Combínalos con el reciclaje de conexiones a nivel de aplicación y, cuando sea posible, con una capa de proxy (PgBouncer, ProxySQL o un balanceador de carga en la nube) que pueda redirigirse independientemente del cacheo del lado del cliente.
Réplicas de lectura que sirven datos obsoletos durante la transición
Durante la ventana entre "la primaria está caída" y "la promoción se completó", el tráfico de lectura suele seguir fluyendo hacia réplicas que ahora son la fuente de datos más actualizada, pero el código de la aplicación con frecuencia asume que las réplicas siempre van un poco atrasadas y enruta en consecuencia. Esto produce una falla confusa: las escrituras fallan, pero las lecturas tienen éxito y devuelven datos "incorrectos" solo porque la aplicación no sabe que la topología acaba de cambiar.
interface DatabaseTopology {
primaryRegion: string;
writableEndpoint: string;
readEndpoints: string[];
lastTopologyChange: Date;
}
// ✅ Centralize topology awareness instead of hardcoding endpoints
class TopologyAwareRouter {
private topology: DatabaseTopology;
async route(query: Query): Promise<string> {
if (query.requiresWrite) {
return this.topology.writableEndpoint;
}
// During transition windows, prefer the writable endpoint for reads too
const secondsSinceChange =
(Date.now() - this.topology.lastTopologyChange.getTime()) / 1000;
if (secondsSinceChange < this.settlingPeriodSeconds) {
return this.topology.writableEndpoint;
}
return this.pickReadEndpoint(this.topology.readEndpoints);
}
private settlingPeriodSeconds = 120;
}La idea central: los cambios de topología deben ser un evento de primera clase ante el cual tu aplicación reacciona, no algo que se infiere a posteriori a partir de errores de conexión.
Cómo probar el failover cuando no hay ningún incendio real
La verdad incómoda es que la mayoría de los procedimientos de failover solo se ponen a prueba durante incidentes reales, que es el peor momento posible para descubrir una suposición equivocada. Los game days —simulacros de failover programados y deliberados— son la única forma confiable de validar toda la cadena: fencing, promoción, propagación de DNS, reciclaje de pools y comportamiento de la aplicación.
| Componente de failover | Cómo probarlo de forma segura | Frecuencia |
|---|---|---|
| Alertas de retraso de replicación | Inyectar carga de escritura artificial y verificar que la alerta se dispare | Mensual |
| Mecanismo de fencing | Simular una partición y confirmar que la primaria anterior rechaza escrituras | Game day trimestral |
| Redirección de DNS/proxy | Medición del tiempo de convergencia entre los pools de clientes | Game day trimestral |
| Lógica de reconexión de la aplicación | Matar conexiones a mitad de transacción y verificar el comportamiento de reintento | Por despliegue (CI) |
| Reconciliación de pérdida de datos | Comparar la posición del WAL en la promoción con la última escritura durable | Game day trimestral |
Ejecutar esto de forma programada, con toda la rotación de guardia involucrada, convierte el failover de una capacidad teórica en un procedimiento ensayado. La primera vez que promuevas una réplica no debería ser durante una interrupción visible para los clientes.
Conclusiones clave
- El retraso es un presupuesto de pérdida de datos, no solo una métrica — define un umbral aceptable antes de necesitarlo, y haz que el failover automatizado lo respete.
- Aplica fencing antes de promover — asumir que la primaria anterior está muerta sin cortarle el acceso de escritura es la forma en que se produce el split-brain.
- El DNS y los pools de conexiones van a la zaga de los cambios de topología — planifica explícitamente para conexiones obsoletas, no dependas solo de los TTL.
- Convierte los cambios de topología en un evento de primera clase para la aplicación — no dejes que las suposiciones de enrutamiento de lectura sirvan silenciosamente datos obsoletos o incorrectos durante las transiciones.
- Ensaya el failover de forma programada — los game days sacan a la luz las suposiciones equivocadas que, de otro modo, los incidentes revelan en el peor momento posible.
- Consigue que el umbral de pérdida de datos lo apruebe el negocio, no solo ingeniería — es una decisión de producto disfrazada de decisión técnica.


