Resiliencia ante caídas del SEE de SUNAT: Arquitectura de contingencia, Outbox Pattern y nuevas normativas de envío
Cómo diseñar plataformas de facturación masiva capaces de absorber caídas de endpoints SOAP de SUNAT/OSE, responder en menos de 120 ms al punto de venta y cumplir con la reducción del plazo de envío a 3 días o 24 horas sin perder comprobantes.
1. La realidad operativa: Caídas recurrentes del servicio SOAP y OSEs
En el ecosistema tributario peruano, el servicio web de recepción de comprobantes electrónicos de SUNAT (billService) y los Operadores de Servicios Electrónicos (OSE) sufren degradaciones frecuentes, especialmente entre los días 10 y 20 de cada mes durante las fechas límite de declaración.
Saturación de gateways de balanceo en la sede electrónica de SUNAT.
La petición SOAP fue enviada pero la conexión cae antes de recibir la Constancia de Recepción (CDR).
Fallas de autenticación LDAP/OAuth central de SUNAT que rechazan credenciales válidas.
2. El impacto crítico de la reducción de plazos de envío (R.S. 000150-2021 y R.S. 000003-2023)
Históricamente, los emisores contaban con hasta 7 días calendario para remitir las facturas electrónicas a SUNAT. Con las resoluciones de superintendencia emitidas por SUNAT:
- Plazo límite de remisión: Se redujo drásticamente a hasta el día calendario siguiente al de emisión (o hasta un máximo de 3 días calendario en periodos de adecuación según perfil de contribuyente).
- Rechazo automático e irreparable: Todo comprobante remitido con posterioridad al plazo legal es catalogado automáticamente como RECHAZADO (código de error 2324 o rechazo definitivo).
- Consecuencia fiscal severa: Una factura rechazada por envío extemporáneo no tiene ninguna validez tributaria: el receptor no puede deducir crédito fiscal ni sustentar costo/gasto, y el emisor no puede reutilizar la serie y correlativo.
REGLA DE ORO DE ARQUITECTURA: El sistema no puede pausar los envíos a la espera de intervención manual. El pipeline de reintentos debe ser autónomo, idempotente y garantizar el envío dentro de la ventana reglamentaria.
3. Patrón Transaccional Outbox: Desacoplar el Punto de Venta
El mayor error en sistemas de facturación principiantes es acoplar la llamada SOAP a SUNAT dentro del ciclo sincrónico de venta del cliente en caja. Si SUNAT demora 15 segundos o se cae, la cola de personas en caja se detiene.
Nuestra arquitectura utiliza el Patrón Outbox:
- El POS/ERP solicita la factura. En menos de 90 ms, el microservicio calcula impuestos, genera el XML UBL 2.1, lo firma con el certificado digital X.509 (XAdES-BES) y guarda el registro con estado
PENDIENTE_DESPACHOen la tabla Outbox. - El cliente recibe su ticket impreso o PDF con su DigestValue (código hash) y código QR normativo, lo cual cumple la exigencia legal de entrega inmediata.
- Un pool de workers asíncronos procesa la tabla Outbox mediante un Circuit Breaker:
// Worker de despacho con Circuit Breaker y Reintento con Jitter
export class SunatDispatchWorker {
private circuitState: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private consecutiveFailures = 0;
private readonly FAILURE_THRESHOLD = 5;
private readonly COOLDOWN_MS = 180_000; // 3 minutos de enfriamiento
private lastStateChange = Date.now();
async dispatchInvoice(invoice: OutboxInvoice): Promise<DispatchResult> {
if (this.circuitState === 'OPEN') {
if (Date.now() - this.lastStateChange > this.COOLDOWN_MS) {
this.circuitState = 'HALF_OPEN';
} else {
// Circuito abierto: encolar y respetar el backoff sin saturar SUNAT
return { status: 'DEFERRED_CIRCUIT_OPEN', retryAfterMs: this.calculateJitter(invoice.attempts) };
}
}
try {
const cdr = await this.soapClient.sendBill({
fileName: invoice.fileName,
contentZipBase64: invoice.zipBase64
});
this.consecutiveFailures = 0;
this.circuitState = 'CLOSED';
return { status: 'ACCEPTED', cdrZip: cdr };
} catch (error: any) {
if (this.isNetworkOrGatewayTimeout(error)) {
this.consecutiveFailures++;
if (this.consecutiveFailures >= this.FAILURE_THRESHOLD) {
this.circuitState = 'OPEN';
this.lastStateChange = Date.now();
}
// Ante timeout ambiguo: registrar estado IN_DOUBT para consulta de validez previa
return { status: 'IN_DOUBT_VERIFY_FIRST', nextCheckMs: this.calculateJitter(invoice.attempts) };
}
throw error;
}
}
// Backoff exponencial con Jitter: evita el 'Thundering Herd' al revivir SUNAT
private calculateJitter(attempt: number): number {
const baseMs = 5000; // 5 segundos base
const maxMs = 300_000; // 5 minutos máximo
const exponential = Math.min(maxMs, baseMs * Math.pow(2, attempt));
const jitter = Math.floor(Math.random() * 2000); // 0 a 2000 ms aleatorio
return exponential + jitter;
}
} 4. Reintentos con Backoff Exponencial y Jitter (Evitar el Thundering Herd)
Cuando la plataforma de SUNAT se restablece tras una caída de 40 minutos, millones de servidores de empresas peruanas intentan reenviar simultáneamente sus comprobantes acumulados. Esto genera una nueva caída inmediata por saturación ("efecto avalancha" o Thundering Herd).
Para mitigar esto, cada reintento aplica un retardo exponencial con dispersión aleatoria:
Esta fluctuación dispersa la carga en el tiempo de forma homogénea, garantizando que el 100% de los comprobantes pendientes ingresen a la sede electrónica sin recibir códigos 503 por saturación.
5. El problema del "Comprobante en Limbo" (Socket Timeout)
¿Qué ocurre si el microservicio envía el ZIP con el comprobante a SUNAT, la llamada tarda 60 segundos y el socket se cierra por timeout?
PELIGRO: REENVÍO CIEGO
Si reenvías inmediatamente sendBill, y SUNAT ya había procesado el XML en su base de datos, responderá con error de duplicidad o rechazo inconsistente.
La solución arquitectónica es la desambiguación previa mediante la API de Consulta Integrada o getStatusCdr:
// Resolución idempotente de comprobantes en estado 'en duda' (Post-Timeout)
export async function resolveInDoubtInvoice(invoice: OutboxInvoice): Promise<InvoiceStatus> {
// 1. Consultar la API REST de Validez Integrada de SUNAT o getStatusCdr
const validationResult = await sunatQueryApi.consultarComprobante({
numRuc: invoice.emisorRuc,
codComp: invoice.tipoComprobante, // '01' Factura, '03' Boleta
numeroSerie: invoice.serie,
numero: invoice.correlativo,
fechaEmision: invoice.fechaEmision,
monto: invoice.totalVenta
});
if (validationResult.estadoComprobante === '1') {
// El comprobante ya fue procesado y ACEPTADO por SUNAT durante el timeout
const cdr = await sunatSoapClient.getStatusCdr({
rucComprobante: invoice.emisorRuc,
tipoComprobante: invoice.tipoComprobante,
serieComprobante: invoice.serie,
numeroComprobante: invoice.correlativo
});
await invoiceRepository.markAccepted(invoice.id, cdr);
return 'ACCEPTED';
}
if (validationResult.estadoComprobante === '0') {
// No existe en los registros de SUNAT: reenvío seguro con idéntico XML y firma
return 'READY_FOR_RETRY';
}
return 'RETRY_LATER';
} 6. Pre-Auditoría en Memoria: Cero comprobantes inválidos en cola
Bajo el nuevo plazo legal estricto de envío, no hay margen para errores de cálculo o de catálogos. Si un comprobante mal formado se queda en la cola de contingencia durante una caída de SUNAT, al momento de restablecerse el servicio el comprobante será rechazado irrevocablemente fuera de plazo.
7. Modo de Contingencia Normada por SUNAT (Caídas Prolongadas)
Cuando la contingencia no es una degradación de red momentánea sino una falla generalizada del sistema (corte de energía prolongado o indisponibilidad total declarada por SUNAT):
- Emisión física de contingencia: Se emiten comprobantes impresos por imprenta autorizada que llevan impresa de fábrica la frase "Comprobante de Pago emitido en contingencia".
- Regularización mediante Resumen de Contingencia: Una vez restablecido el sistema, se remite a SUNAT el Resumen de Comprobantes Impresos en Contingencia mediante el método SOAP
sendSummary, obteniendo un ticket asíncrono para verificar su aceptación.
[ CONCLUSIONES DE PRODUCCIÓN PARA MÁS DE 30 EMPRESAS ]
La implementación conjunta de este patrón Outbox con Circuit Breaker, pre-auditoría en memoria y desambiguación idempotente permitió:
- ✔ 0% de facturas bloqueadas o perdidas por caídas de SUNAT.
- ✔ 100% de cumplimiento del plazo legal de remisión de 24 a 72 horas.
- ✔ Tiempo de respuesta constante en punto de venta < 100 ms independientemente del estado de SUNAT.