Todo dia 1º do mês às 08h00 da manhã — quando os CRON jobs de faturamento recorrente de SaaS, escolas, clubes de assinatura e escritórios contábeis disparam juntos — e toda sexta-feira de Black Friday no varejo, eu vejo exatamente o mesmo antipadrão derrubar bancos de dados de ERPs inteiros: chamadas HTTP síncronas para emissão fiscal dentro de uma transação de banco de dados ou de um for...of bloqueante.
Eu já passei por isso antes de fundarmos a Notaas. Em testes de bancada com 10 notas, fazer await emitirNota() dentro da rota de checkout ou de um loop simples parece funcionar perfeitamente. Mas quando um cliente do seu ERP seleciona 800 faturas na tela e clica em "Emitir Notas do Mês em Lote", ou quando 40 lojas de uma rede de franquias passam 120 vendas por minuto no PDV, a física dos sistemas distribuídos cobra seu preço.
Neste artigo técnico, vou abrir a engenharia de como projetamos o pipeline de alta vazão da Notaas e como você deve arquitetar o lado do seu ERP ou SaaS para processar milhares de emissões de NFS-e Nacional, NF-e (Modelo 55) e NFC-e (Modelo 65) sem esgotar conexões do PostgreSQL, sem sofrer com head-of-line blocking entre lojas e com validação criptográfica de webhooks HMAC SHA-256 em menos de 50ms.
Por que chamadas HTTP síncronas em loop travam o banco do ERP no fechamento do mês
Para entender por que um loop síncrono de emissão fiscal derruba até servidores robustos, precisamos olhar para a assimetria de latência entre o seu banco de dados e os autorizadores governamentais (SEFAZ estaduais, SVRS, SNNFSE/ADN e prefeituras municipais).
Uma query SQL no PostgreSQL leva de 1ms a 5ms. Já uma transmissão mTLS com assinatura XML RSA-SHA1/SHA256 contra um webservice da SEFAZ ou prefeitura leva, em dias normais, de 400ms a 2.500ms — e pode chegar a 15 segundos quando uma prefeitura do interior ou um autorizador estadual entra em degradação parcial.
Quando um desenvolvedor escreve um código que abre uma transação no banco (BEGIN), reserva o próximo número de série da nota (SELECT ... FOR UPDATE), chama a API fiscal aguardando a resposta síncrona e só depois executa o COMMIT, três efeitos em cascata acontecem sob carga:
- Exaustão do Connection Pool (Database Starvation): Se o pool do Prisma, Drizzle ou TypeORM tem 20 conexões e 20 requisições ficam penduradas esperando um webservice municipal responder por 8 segundos, todo o restante do seu ERP para de responder — inclusive telas de login e consultas simples que não têm nada a ver com nota fiscal.
- Contenção de Lock por CNPJ e Race Condition de Numeração: Duas threads tentando emitir nota para o mesmo CNPJ ao mesmo tempo disputam o ponteiro de numeração de RPS ou de NF-e. Se não houver serialização por emitente, duas notas saem com o mesmo número e geram erro de duplicidade, ou um buraco na sequência numérica exige inutilização posterior.
- Efeito Vizinho Barulhento (Noisy Neighbor / Head-of-Line Blocking): Se o seu sistema usa uma única fila FIFO global para todos os seus clientes, um lote de 1.000 notas de um cliente em um município cujo webservice está lento congela na fila a emissão de uma única NFC-e de outro cliente cujo PDV precisava liberar o consumidor no caixa em 2 segundos.
Regra de ouro da engenharia fiscal em escala: você nunca segura uma transação de banco de dados aberta enquanto espera um órgão público assinar um XML.
Emissão em Lote (POST /api/v1/emitir/lote) vs. Fila Assíncrona com externalId Determinístico
Aqui na Notaas, desenhamos dois caminhos complementares para resolver a emissão em alta escala, dependendo de como o seu sistema gera a demanda fiscal:
1. O Endpoint Nativo de Emissão em Lote (POST /api/v1/emitir/lote)
Quando o seu usuário clica em "Faturar 200 contratos selecionados" no fechamento contábil ou quando um script de billing roda à meia-noite, abrir 200 conexões TCP/TLS seguidas da sua infraestrutura para a API fiscal desperdiça banda e CPU.
Com o endpoint POST /api/v1/emitir/lote, o seu backend envia um único payload JSON contendo um array de documentos fiscais. O motor de ingestão da Notaas valida o schema do lote inteiro instantaneamente, grava os itens com garantia de idempotência pelo externalId de cada item, devolve HTTP 202 Accepted com o batchId em poucos milissegundos e distribui o processamento assíncrono internamente.
Veja um exemplo real de requisição para POST /api/v1/emitir/lote disparando múltiplas notas de uma vez (e herdando automaticamente os parâmetros de IBS/CBS configurados no projeto, conforme detalhamos no nosso guia sobre o Anexo VIII e a Reforma Tributária):
{
"ambiente": "producao",
"batchReference": "FECHAMENTO-MENSAL-2026-10-LOJA-01",
"webhookUrl": "https://api.seuerp.com.br/webhooks/notaas",
"items": [
{
"externalId": "order_tnt99_88410_v1",
"tipo": "nfse",
"prestador": { "cnpj": "12345678000199" },
"tomador": {
"cpfCnpj": "44556677000122",
"razaoSocial": "Tech Corp Soluções Ltda",
"email": "financeiro@techcorp.com.br",
"endereco": {
"logradouro": "Av. Paulista",
"numero": "1578",
"bairro": "Bela Vista",
"codigoMunicipio": "3550308",
"uf": "SP",
"cep": "01310200"
}
},
"servico": {
"codigo": "010501",
"descricao": "Assinatura Mensal Plataforma Enterprise - Outubro/2026",
"valor": 1490.00,
"aliquota": 2.0
}
},
{
"externalId": "order_tnt99_88411_v1",
"tipo": "nfse",
"prestador": { "cnpj": "12345678000199" },
"tomador": {
"cpfCnpj": "77889900000155",
"razaoSocial": "Varejo Express Brasil S.A.",
"email": "fiscal@varejoexpress.com.br",
"endereco": {
"logradouro": "Rua XV de Novembro",
"numero": "400",
"bairro": "Centro",
"codigoMunicipio": "4106902",
"uf": "PR",
"cep": "80020310"
}
},
"servico": {
"codigo": "010501",
"descricao": "Assinatura Mensal Plataforma Enterprise - Outubro/2026",
"valor": 2990.00,
"aliquota": 2.0
}
}
]
}
2. Isolamento Multi-Loja nos Workers RabbitMQ da Notaas
Uma dúvida comum de arquitetos de software que avaliam nosso comparativo de APIs de emissão de NF-e, NFS-e e NFC-e ou gerenciam dezenas de empresas via Org API multi-CNPJ é: "Se uma loja da minha rede mandar um lote de 500 NFS-e para uma prefeitura lenta, as outras 30 lojas que estão emitindo NFC-e no caixa vão ficar esperando na fila?"
Na Notaas, a resposta é não, porque nós separamos as topologias de filas em dois níveis:
- Segregação por Modalidade e Criticidade: Documentos de varejo presencial (
NFC-eModelo 65) trafegam em fast-lanes dedicadas de baixa latência, totalmente isoladas das filas de lotes deNFS-emunicipal. - Particionamento e Lock Atômico por CNPJ + Série: O controle sequencial de numeração de RPS/NF-e ocorre exclusivamente dentro do escopo
(projectId, modelo, serie). Isso significa que 50 CNPJs diferentes da sua organização processam notas em paralelo real nos nossos workers RabbitMQ, enquanto notas do mesmo CNPJ recebem numeração sequencial atômica no Redis/PostgreSQL sem risco de corrida.
⚡ Fluxo Arquitetural de Emissão em Alta Escala (ERP + Notaas):
- 1. ERP / Checkout: Grava o pedido no banco local com status
PENDING_INVOICE, gera oexternalIddeterminístico (order_${tenantId}_${orderId}_v1) e enfileira o job no BullMQ local em menos de 5ms.- 2. BullMQ Worker (Lado do ERP): Agrupa faturas em lotes (
POST /api/v1/emitir/lote) ou disparaPOST /api/v1/emitircom retry exponencial para erros de rede (5xx), nunca duplicando notas graças aoexternalId.- 3. Notaas Engine (RabbitMQ Partitioned Workers): Aloca o número sequencial por CNPJ/série atomicamente, assina o XML com o Certificado A1 em memória, transmite para SEFAZ/ADN e gera localmente o DANFE / DANFSe v2.0 em PDF.
- 4. Webhook Assinado (
X-Notaas-Signature): A Notaas dispara umPOSTpara o seu servidor com o eventoinvoice.issuedassinado via HMAC SHA-256.- 5. Ack Rápido (< 50ms) & Atualização: Seu endpoint valida o HMAC com
crypto.timingSafeEqual, responde200 OKimediatamente e atualiza o pedido no banco com os links de XML e PDF.
Implementação Completa em TypeScript: BullMQ Producer, Worker e Idempotência Determinística
Vamos construir o lado do seu SaaS/ERP em Node.js e TypeScript usando BullMQ (sobre Redis).
O primeiro pilar é a função geradora do externalId determinístico. Nunca use uuidv4() aleatório gerado dentro do worker de retry! Se a primeira tentativa der timeout de rede após chegar na API e o seu worker gerar um novo UUID na segunda tentativa, você emitirá duas notas fiscais para a mesma venda. O externalId deve ser uma função pura dos dados imutáveis da operação:
import { Queue, Worker, Job } from "bullmq";
import IORedis from "ioredis";
const redisConnection = new IORedis(process.env.REDIS_URL || "redis://127.0.0.1:6379", {
maxRetriesPerRequest: null,
});
export interface InvoiceJobPayload {
tenantId: string;
tenantApiKey: string;
orderId: string;
attemptVersion: number; // Incrementado APENAS se o usuário corrigir dados cadastrais após rejeição 4xx
documento: {
tipo: "nfse" | "nfe" | "nfce";
prestadorCnpj: string;
tomador: {
cpfCnpj: string;
razaoSocial: string;
email: string;
endereco: {
logradouro: string;
numero: string;
bairro: string;
codigoMunicipio: string;
uf: string;
cep: string;
};
};
servico: {
codigo: string;
descricao: string;
valor: number;
aliquota: number;
};
};
}
/**
* Gera uma chave de idempotência determinística.
* Se o BullMQ tentar reenviar 5 vezes por falha de rede, o externalId será idêntico
* e a Notaas garantirá que apenas 1 nota fiscal seja emitida.
*/
export function buildDeterministicExternalId(
tenantId: string,
orderId: string,
attemptVersion = 1
): string {
return `order_${tenantId}_${orderId}_v${attemptVersion}`;
}
export const fiscalEmissionQueue = new Queue<InvoiceJobPayload>("fiscal-emission-queue", {
connection: redisConnection,
defaultJobOptions: {
attempts: 6,
backoff: {
type: "exponential",
delay: 2000, // 2s, 4s, 8s, 16s, 32s, 64s
},
removeOnComplete: 1000,
removeOnFail: 5000,
},
});
// Worker que consome a fila com concorrência controlada
export const fiscalEmissionWorker = new Worker<InvoiceJobPayload>(
"fiscal-emission-queue",
async (job: Job<InvoiceJobPayload>) => {
const { tenantId, tenantApiKey, orderId, attemptVersion, documento } = job.data;
const externalId = buildDeterministicExternalId(tenantId, orderId, attemptVersion);
const response = await fetch("https://platform.notaas.com.br/api/v1/emitir", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": tenantApiKey,
},
body: JSON.stringify({
externalId,
tipo: documento.tipo,
ambiente: "producao",
prestador: { cnpj: documento.prestadorCnpj },
tomador: documento.tomador,
servico: documento.servico,
}),
signal: AbortSignal.timeout(10000), // Timeout de transporte HTTP de 10s
});
// Erros 4xx (exceto 429 Rate Limit) são erros de validação fiscal/cadastral.
// Não devemos fazer retry automático no BullMQ sem que o usuário corrija o cadastro.
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
const errorPayload = await response.text();
job.discard(); // Impede retries inúteis de nota com CPF/endereço inválido
throw new Error(`Erro permanente de validação (${response.status}): ${errorPayload}`);
}
if (!response.ok) {
throw new Error(`Erro transitório HTTP ${response.status} — BullMQ agendando retry.`);
}
return response.json();
},
{
connection: redisConnection,
concurrency: 15, // Processa até 15 envios simultâneos sem sobrecarregar o banco local
}
);
Repare no detalhe importante dentro do worker: quando a API retorna um erro 400 ou 422 (por exemplo, CEP inexistente ou CPF inválido do tomador), chamamos job.discard() antes de lançar a exceção. Isso evita que o seu worker gaste 6 tentativas reenviando um payload que sabemos que precisa de correção humana ou cadastral. Já se houver um erro 502/503/504 ou timeout de rede, o BullMQ reenvia automaticamente usando exatamente o mesmo externalId.
Segurança e Performance no Retorno: Validando Webhooks HMAC SHA-256 com crypto.timingSafeEqual
Depois que o lote ou a fila envia as notas para a Notaas, os resultados voltam de forma assíncrona para a URL de webhook do seu sistema à medida que cada prefeitura ou SEFAZ autoriza os documentos.
Existem duas falhas graves que vejo em endpoints de webhook de sistemas fiscais:
- Falta de verificação criptográfica da assinatura (
X-Notaas-Signature): Sem validar o HMAC sobre o corpo bruto (raw body) da requisição, qualquer atacante que descubra a URL do seu webhook pode forjar um payload JSON fingindo que uma nota foi autorizada ou cancelada. - Processamento pesado antes do
200 OK: Se o seu endpoint de webhook tenta baixar o PDF, enviar e-mail via SMTP lento e gerar boletos antes de responder200 OKpara a Notaas, em momentos de pico de lote (quando 300 notas são autorizadas no mesmo minuto) o seu endpoint vai dar timeout.
Veja abaixo a implementação de referência em TypeScript (compatível com Next.js App Router ou Express) que valida o cabeçalho X-Notaas-Signature usando crypto.timingSafeEqual (prevenindo timing attacks) e responde em menos de 50ms:
import crypto from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
const WEBHOOK_SECRET = process.env.NOTAAS_WEBHOOK_SECRET!;
export interface NotaasWebhookEvent {
eventId: string;
event: "invoice.issued" | "invoice.error" | "invoice.cancelled";
timestamp: string;
projectId: string;
data: {
id: string;
externalId: string;
tipo: "nfse" | "nfe" | "nfce";
numero: string;
serie: string;
chaveAcesso: string;
protocolo: string;
xmlUrl: string;
pdfUrl: string; // DANFE ou DANFSe v2.0 gerado localmente pela Notaas
errorMessage?: string;
errorCode?: string;
};
}
/**
* Valida a assinatura HMAC SHA-256 em tempo constante (proteção contra Timing Attack)
*/
export function verifyNotaasSignature(
rawBody: string,
signatureHeader: string | null,
secret: string
): boolean {
if (!signatureHeader || !secret) return false;
// Suporta formato "sha256=abcdef..." ou hash hexadecimal direto
const receivedHex = signatureHeader.startsWith("sha256=")
? signatureHeader.slice(7)
: signatureHeader;
const expectedHmac = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const receivedBuffer = Buffer.from(receivedHex, "hex");
const expectedBuffer = Buffer.from(expectedHmac, "hex");
if (receivedBuffer.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}
export async function POST(req: NextRequest) {
// 1. Lê o rawBody ANTES de fazer JSON.parse para garantir integridade do hash
const rawBody = await req.text();
const signature = req.headers.get("x-notaas-signature");
if (!verifyNotaasSignature(rawBody, signature, WEBHOOK_SECRET)) {
return NextResponse.json(
{ error: "Assinatura HMAC inválida (X-Notaas-Signature)" },
{ status: 401 }
);
}
const payload = JSON.parse(rawBody) as NotaasWebhookEvent;
// 2. Idempotência de recebimento de webhook (UPSERT rápido por externalId / eventId)
// Aqui você apenas grava o status e as URLs de XML/PDF no banco em < 15ms
// e delega envio de e-mail/WhatsApp para um worker secundário.
await updateOrderFiscalStatusFast({
externalId: payload.data.externalId,
status: payload.event === "invoice.issued" ? "AUTHORIZED" : "REJECTED",
invoiceNumber: payload.data.numero,
accessKey: payload.data.chaveAcesso,
xmlUrl: payload.data.xmlUrl,
pdfUrl: payload.data.pdfUrl,
errorDetails: payload.data.errorMessage,
});
// 3. Responde 200 OK em menos de 50ms
return NextResponse.json({ received: true, eventId: payload.eventId }, { status: 200 });
}
async function updateOrderFiscalStatusFast(params: {
externalId: string;
status: "AUTHORIZED" | "REJECTED";
invoiceNumber?: string;
accessKey?: string;
xmlUrl?: string;
pdfUrl?: string;
errorDetails?: string;
}) {
// Exemplo de UPDATE indexado pela coluna UNIQUE external_id no PostgreSQL
console.log(`[WEBHOOK ACK] Pedido ${params.externalId} -> ${params.status} (PDF: ${params.pdfUrl})`);
}
Um detalhe valioso dessa arquitetura: quando o evento invoice.issued chega no seu webhook, a propriedade pdfUrl já entrega o link direto do DANFSe v2.0 (NT-008) ou do DANFE gerado localmente pelos motores da Notaas. O seu worker de notificação não precisa fazer nenhuma chamada adicional ao visualizador público do Ambiente Nacional (adn.nfse.gov.br), que costuma falhar justamente nos dias de pico de faturamento.
Perguntas Frequentes sobre Emissão em Lote, Filas e Webhooks
1. Qual é o limite de notas por chamada no endpoint de emissão em lote (POST /api/v1/emitir/lote)?
Recomendamos enviar lotes de até 100 a 250 documentos fiscais por requisição HTTP no endpoint POST /api/v1/emitir/lote. Se o seu fechamento mensal tiver 5.000 notas para emitir, basta fatiar o array em chunks de 200 itens. Isso mantém o payload HTTP leve (abaixo de 1 MB), garante resposta 202 Accepted imediata e permite que os workers da Notaas processem todos os itens paralelizados por CNPJ.
2. O que acontece se o meu servidor cair no momento em que a Notaas disparar o webhook invoice.issued?
A Notaas possui uma política automática de retentativas com exponential backoff para entrega de webhooks. Se o seu endpoint não responder com status HTTP 2xx dentro da janela de timeout (ou retornar 500/502/503), nosso motor reenfileira a entrega do evento automaticamente em intervalos crescentes, mantendo o histórico completo de tentativas e payloads no painel da plataforma para reenvio com 1 clique.
3. Como testar filas de emissão e webhooks HMAC sem gastar franquia de produção?
Ao criar sua conta na Notaas no Plano Free, você recebe acesso imediato para criar até 5 projetos (CNPJs) simultâneos grátis em Homologação via Org API, além de 20 notas fiscais gratuitas por mês em Produção real para 1 CNPJ. Você pode disparar lotes inteiros em homologação ("ambiente": "homologacao") e validar a assinatura X-Notaas-Signature no seu ambiente local ou de staging sem pagar nada e sem cadastrar cartão de crédito.
Escale sua Operação Fiscal Sem Travar o Banco do seu ERP
Chega de sofrer com timeouts no fechamento do dia 1º ou na Black Friday. Integre seu sistema com a API fiscal desenhada por engenheiros para alta concorrência, emissão em lote, idempotência nativa e DANFSe v2.0 local:
👉 Criar Conta Gratuita na Notaas (20 Notas/Mês em Produção + 5 CNPJs em Homologação)
👉 Acessar a Documentação Técnica de Lotes, Idempotência e Webhooks

