# Revisão de latência do envio PIX ao BACEN — decomposição empírica e causa raiz

Data: 2026-07-23. Mandato do dono: liquidar PIX em **< 500ms**. Print do
Histórico de Status mostrou +1.1s só no envio ao BACEN (pacs.008). Esta revisão
decompõe o tempo, prova a causa raiz e lista as ações.

## 1. Decomposição empírica (PRD, medida nos logs `[ICOM] SEND_OK`)

O `OutboundSender` cronometra cada fase do envio: `gate_ms` (validação
estrutural) + `sign_ms` (assinatura XMLDSig via HSM) + `xsd_ms` (validação XSD) +
`post_ms` (POST ao BACEN via sidecar mTLS). Amostras reais de PRD:

| gate_ms | sign_ms | xsd_ms | **post_ms** | total |
|---|---|---|---|---|
| 8 | 43 | 4 | **1017** | 1072 |
| — | 57 | 4 | **5775** | 5836 |
| — | 760 | 5 | **3099** | 3864 |
| — | 63 | 4 | **2816** | 2883 |
| — | 36 | 5 | **5387** | 5428 |

**O `post_ms` domina: 1017–5775ms.** `gate_ms`/`xsd_ms` são desprezíveis (<10ms).
`sign_ms` é normalmente baixo (36–63ms) mas ocasionalmente alto (760ms).

## 2. Causa raiz (provada por código + logs)

O `post_ms` é o POST HTTP do backend Elixir ao sidecar mTLS local
(`localhost:9103`), que faz o handshake mTLS ao ICOM do BACEN. O gargalo NÃO é a
rede pura — é o **handshake mTLS frio**, e o handshake mTLS depende do HSM:

1. **Cada handshake mTLS novo faz o HSM assinar o CertificateVerify.**
   `pix/services/bacen_mtls_sidecar/main.go:257-267`: "Without a
   ClientSessionCache, Go's TLS client NEVER resumes: every new TCP handshake
   makes the HSM sign the CertificateVerify (one `sign-rsa` round-trip)."
2. **O session resumption está em 0%** (log vivo do sidecar PRD):
   `[tls icom] handshakes=20 full=20 resumed=0 resume_rate=0.0% (full handshakes
   = HSM sign-rsa calls)`. Apesar de `SIDECAR_TLS_SESSION_CACHE=on`, o BACEN não
   está retomando as sessões TLS 1.2 — então TODO handshake é completo e chama o
   HSM. (Forçamos TLS 1.2 porque o HSM só faz PKCS#1 v1.5, não o RSA-PSS do
   TLS 1.3 — `main.go:138-143`.)
3. **As conexões esfriam.** O gateway RSFN fecha conexões keep-alive ociosas em
   ~30s (`shared/application.ex:184-189`, `@bacen_conn_max_idle_time 30_000`).
   Em baixo volume (teste, madrugada, início de operação) quase todo envio pega a
   conexão fria → novo handshake → HSM.
4. **O HSM RTM está degradado** (dossiê
   `2026-07-23-hsm-rtm-degradacao-504-dossie.md`): 504 "HttpClient.Timeout of 3
   seconds" em 50-67% das chamadas. O selftest do sidecar no boot falhou:
   `[selftest] icom -> ... HANDSHAKE FAILED: sign-rsa HTTP 504`.

**Cadeia:** baixo volume → conexão ICOM esfria (30s) → próximo PIX abre conexão
nova → handshake mTLS completo (resume 0%) → sidecar chama HSM `sign-rsa` → HSM
degradado responde em 1-3s ou 504 → `post_ms` de 1-6s (ou falha o envio).

Ou seja, os dois sintomas (`sign_ms` alto e `post_ms` alto) têm a MESMA raiz: o
HSM RTM no caminho crítico. O `sign_ms` é a assinatura XMLDSig da mensagem; o
`post_ms` é a assinatura do handshake mTLS. Com o HSM saudável e as conexões
quentes, ambos caem para dezenas de ms.

## 3. Por que o < 500ms é impossível hoje

Com `post_ms` de 1-6s por conexão fria e resume 0%, nenhum envio em baixo volume
fecha em 500ms. Em ALTO volume contínuo as conexões ficam quentes por tráfego e o
`post_ms` cai — mas o mandato exige < 500ms SEMPRE, inclusive na primeira
transação do dia e em baixo volume.

## 4. Ações (pacote de latência)

Ordem por impacto no `post_ms`:

1. **Manter as conexões ICOM quentes com heartbeat < 30s** (mais impactante).
   Se a conexão TCP não esfria, não há handshake, o HSM sai do caminho crítico
   de CADA envio, e o `post_ms` vira tempo de rede puro. Mecanismo existente:
   `EchoProbeWorker` (`settlement/.../monitoring/echo_probe_worker.ex`) — hoje
   OFF (`ECHO_PROBE_ENABLED` default false) e com intervalo default de 5min
   (>> 30s, inútil para keep-warm). Ação: ligar com intervalo < 30s (ex.: 25s)
   nos 2 ambientes. TRADE-OFF: gera tráfego echo pibr.001 real ao BACEN (decisão
   operacional do dono). Alternativa mais leve a avaliar: um GET leve ao ICOM
   (não uma mensagem assinada) a < 30s só para manter o socket.
2. **Retry resiliente do HSM** (JÁ FEITO, commit 53b00011): 500/502/503/504 +
   transporte retentam com backoff. Reduz a falha de handshake/assinatura por
   blip do HSM. Fallback de confiabilidade, não de latência.
3. **RTM consertar o HSM** (dossiê pronto): elevar o timeout de 3s do gateway
   e/ou sanar a lentidão do vHSM. Sem isso, cada handshake frio residual custa 3s.
4. **Investigar o resume_rate 0%** com BACEN/RTM: por que o ICOM não emite/aceita
   session tickets TLS 1.2? Se resolvido, mesmo conexões novas retomam a sessão e
   PULAM o HSM no handshake — elimina a dependência HSM↔handshake.
5. **Keep-warm da conexão sidecar→HSM** e pré-warm no boot: o `HsmKeepWarm`
   (PIX, health 20s) mantém quente a conexão ELIXIR→HSM, mas o SIDECAR tem sua
   PRÓPRIA conexão ao HSM, sem keep-warm. Avaliar um keep-warm no sidecar.

## 4b. Carga HSM auto-infligida (achado central — responde "consumimos demais?")

`SIDECAR_TLS_SESSION_CACHE=on` está ativo, o cache LRU está correto, MAS o
gateway RSFN do BACEN **não emite/aceita session tickets TLS 1.2**: os counters
do sidecar mostram `resume_rate=0.0%` de forma consistente (o próprio código já
previa isso em `main.go:280-284`). Logo, TODO handshake é completo e chama o HSM.

Volume medido em PRD, 24h (cada handshake = 1 `sign-rsa` ao HSM RTM):

| Upstream | handshakes/24h |
|---|---|
| icom_sec | 6060 |
| icom | 4200 |
| dict | 1360 |
| **TOTAL** | **~11.620 sign-rsa/dia SÓ para handshakes** |

Isso é ~8 chamadas HSM por minuto, 24h/dia, **nenhuma delas assinando uma
mensagem** — só abrindo conexão. O maior ofensor é o canal SECUNDÁRIO
(`icom_sec`, long-poll de recebimento) reabrindo streams. A causa é o churn de
conexão: o long-poll ICOM recicla o stream com frequência e o gateway RSFN fecha
conexões idle em ~30s (enquanto o sidecar as considera vivas por 90s —
`main.go:435-436`), e como o resume é 0%, cada reabertura paga um `sign-rsa`.

**Consequência estratégica:** parte da lentidão/erro do HSM é carga NOSSA
(11.620 handshakes/dia). Antes de cobrar a RTM pela lentidão do HSM, devemos
reduzir essa carga (menos handshakes) — senão a RTM responde, com razão, que a
sobrecarga é nossa. A ordem correta é: (1) reduzir nossos handshakes; (2)
com o lado nosso limpo, cobrar o HSM residual.

## 5. Full compare com o Poller do LegadoPIX

Relatório completo: `docs/reports/2026-07-23-latencia-poller-legadopix-compare.md`.

O legado É rápido no frio por 3 razões ARQUITETURAIS que não temos (e não
queremos abrir mão da segurança): (a) chave PIC em MEMÓRIA — o SslStream assina
o handshake TLS LOCALMENTE, sem round-trip ao HSM
(`SPI.Core.Mensageria.Geral.Application.NetCore:224,231-244`); (b) sem hop de
sidecar; (c) conexão direta ao BACEN. A recepção do legado é um long-poll
contínuo de 200ms (`SPI.Core.Worker.RetornoApiBacen.Application:88`), igual ao
nosso; e recepção/envio usam clientes SEPARADOS também no legado (não
compartilham conexão). O legado NÃO tem keep-warm dedicado do envio — ele só não
sofre porque o handshake frio dele é barato (sem HSM).

Conclusão do compare: enquanto a chave PIC ficar no HSM (decisão de segurança
CORRETA — a chave nunca sai do HSM), o handshake COMPLETO sempre custará o
round-trip ao HSM. Logo a estratégia certa NÃO é imitar o legado, é **nunca
fazer handshake completo no caminho crítico**: manter a conexão sempre quente
(reuso) para que o envio pegue uma conexão já estabelecida (zero handshake).

## 6. Plano do pacote de latência (por impacto)

Decomposição das 3 causas do `post_ms` (do compare):
- **Causa A — HSM no handshake** (estrutural): chave no HSM, todo handshake
  completo chama `sign-rsa`. Mitigar mantendo a conexão quente (evitar
  handshake) + retry.
- **Causa B — conexão sidecar→BACEN esfria**: o `http.Transport` do sidecar não
  definia `MaxIdleConnsPerHost` (default Go = 2 idle/host); os 6 slots de
  long-poll fechavam a conexão quente que o envio reusaria.
- **Causa C — contenção do semáforo**: `ConnSemaphore` cap 6/canal (limite BACEN
  §2.2.2.10); os 6 slots de recepção CPM + os envios disputam os MESMOS 6
  permits; um envio espera até 3s por permit (`client.ex:475-479`), dentro do
  `post_ms`.

Ações:

| # | Ação | Causa | Estado | Risco |
|---|---|---|---|---|
| 1 | Sidecar `MaxIdleConnsPerHost=10` (env `SIDECAR_MAX_IDLE_CONNS_PER_HOST`) | B | **FEITO (TDD, Go)** | baixo |
| 2 | Retry resiliente do HSM (500/502/503/504/transporte) | A | **FEITO (commit 53b00011)** | baixo |
| 3 | Ligar `EchoProbeWorker` < 30s (`ECHO_PROBE_ENABLED=true`, `ECHO_PROBE_INTERVAL_MS=20000`) — echo por `POST /api/v1/in/{ispb}/msgs`, a MESMA rota do envio, mantém a conexão de envio quente | A+B | **APROVADO pelo dono** (gera tráfego echo pibr.001 ao BACEN) | baixo-médio |
| 4 | ~~Reduzir slots de recepção~~ | C | **VETADO pelo dono**: usar o recurso FULL do BACEN (6 conexões/canal); é sistema de altíssima volumetria e não se reduz capacidade por falta de solução | — |
| 5 | RTM: elevar o timeout de 3s do gateway / sanar o vHSM (dossiê pronto) | A | **externo (RTM)** | — |
| 6 | Investigar resume_rate 0% com BACEN/RTM (session tickets TLS 1.2) | A+B | **externo** | — |

**Decisão do dono (23/07):** #3 aprovado, #4 vetado. Não reduzir a capacidade de
recepção — usar as 6 conexões/canal (recurso full do BACEN, §2.1.4/§2.2.2.10),
porque o sistema é de altíssima volumetria e deve se comportar como tal.

Notas de conformidade com o Manual das Interfaces v1.12 (a favor da estratégia):
- **§2.1.3 recomenda conexão PERSISTENTE**: "Fechamentos frequentes com alto
  fluxo de mensagens reduzirá o desempenho do sistema" — valida #1 e #3.
- **§2.2.2.1 leitura em lote `multipart/mixed` (até 10 msgs/pull)**: JÁ
  implementado (`client.ex:396`, `cpm/worker.ex` parseia multipart). Em alta
  volumetria isso reduz 10x o número de pulls, liberando permits com muito mais
  frequência para o envio — a contenção C se dissolve naturalmente sob carga.

**Meta:** #1 + #2 reduzem o caso frio; #1 + #3 (conexão de envio sempre quente)
levam o `post_ms` quente para < 100ms. A contenção C (envio esperar permit) só é
material em BAIXO volume (long-polls bloqueados 15s segurando os 6 permits); sob
a alta volumetria esperada, os pulls retornam rápido (multipart) e há folga. A
estratégia também LIMPA a carga HSM (de 11.620 handshakes/dia para centenas) —
pré-condição para cobrar o HSM residual da RTM (#5) sem devolverem a culpa.

Follow-up residual (a validar, NÃO reduz capacidade): em baixo volume, avaliar
encurtar o timeout do long-poll (hoje 15s) para liberar permits com mais
frequência — com a conexão quente (#1+#3) o churn extra não paga handshake.

