TL;DR: Se você está integrando IA (LLMs, etc.) no backend e fazendo várias chamadas externas de forma sequencial, está jogando tempo fora. Ao aplicar noções básicas de concorrência e paralelismo (Promise.all, limitação de concorrência, separação entre ingestão externa e leitura paginada), é perfeitamente plausível reduzir um endpoint de ~40s para ~8s sem trocar de stack — só usando fundamentos de computação.
Concorrência, paralelismo e IA em produção: como reduzir a latência de endpoints de 40s para 8s
Nos últimos anos, ficou relativamente fácil “plugar” IA generativa em qualquer backend: basta chamar uma API, mandar o texto e receber a resposta. O problema é que, em produção, esse “basta chamar” rapidamente vira 40 segundos de espera num endpoint que deveria responder em poucos segundos.
O que separa um endpoint lento e frágil de um endpoint robusto e rápido, especialmente quando envolve IA externa, não é “misticismo de prompt engineering”. É fundamento de computação: processos, threads, concorrência, paralelismo, latência e throughput — conceitos que aparecem em livros clássicos de Sistemas Operacionais e Programação Concorrente, como Tanenbaum, Herlihy & Shavit, Goetz, Herb Sutter, etc.
Este artigo cria um cenário hipotético (não ligado a nenhum domínio específico) para mostrar, passo a passo, como aplicar esses conceitos em um backend Node.js/TypeScript que:
- faz múltiplas chamadas a APIs externas de IA;
- consulta serviços legados internos;
- e reduz a latência de um endpoint de ~40s para ~8s, sem mudar de linguagem, apenas usando melhor concorrência, paralelismo e arquitetura.
A ideia é que você consiga reaproveitar os princípios em qualquer projeto com IA.
1. Cenário hipotético: agregador de relatórios inteligentes
Imagine um SaaS chamado ReportX.
Um cliente chama o endpoint:
GET /api/v1/reports/user/:userId?limit=5&page=1
Esse endpoint precisa:
- Buscar os últimos N registros de atividade do usuário em sistemas internos (banco próprio, serviço legado, etc.).
- Para cada registro, chamar uma API de IA externa (LLM) para gerar um resumo inteligente.
- Agregar tudo num JSON de resposta paginado.
Versão ingênua:
- buscar registros internos de forma sequencial;
- para cada registro, chamar a IA sequencialmente;
- só então responder.
Se cada chamada de IA leva ~2s, e você faz 5–10 chamadas em sequência, qualquer coisa que envolva IO adicional (DB, HTTP interno, etc.) empurra o tempo total facilmente para 30–40s.
Nosso objetivo: manter a mesma lógica de negócio, mas reduzir drasticamente a latência usando:
- concorrência controlada (várias chamadas IO em paralelo);
- paralelismo real quando necessário (trabalho CPU-bound, se houver);
- e algumas estratégias arquiteturais simples.
2. Concorrência vs paralelismo (e por que isso importa para IA)
Uma forma clássica de separar os conceitos:
- Concorrência: lidar com muitas coisas acontecendo ao mesmo tempo (estrutura e coordenação de tarefas).
- Paralelismo: executar várias coisas literalmente em paralelo (ao mesmo tempo em múltiplos núcleos).
Em aplicações web típicas:
- chamadas de banco, HTTP, IA externa → são tarefas I/O-bound ⇒ se beneficiam de concorrência (não bloquear enquanto esperam resposta);
- operações intensivas de CPU (compressão, criptografia, parsing pesado, análise de grandes estruturas) → são CPU-bound ⇒ podem se beneficiar de paralelismo (worker threads, outros processos).
Herb Sutter cunhou o famoso “The free lunch is over” para explicar que simplesmente esperar por CPUs mais rápidas já não resolve; aproveitamento de múltiplos núcleos e modelos de concorrência tornam-se obrigatórios.
Em um endpoint que fala com IA:
- o gargalo principal costuma ser IO externo (latência de rede até o provedor de IA);
- portanto, concorrência bem usada (várias chamadas em paralelo, com limites) quase sempre gera ganhos gigantes de performance.
3. Versão ingênua: loop sequencial com IA externa
Comecemos com uma implementação simplificada (em TypeScript/Node) do endpoint do ReportX:
// services/aiClient.ts
export async function summarizeWithAI(text: string): Promise<string> {
// Chamada genérica a um provedor de IA (LLM)
const response = await fetch("https://api.ia-externa.com/v1/summarize", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.IA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "awesome-llm",
text,
max_tokens: 200,
}),
});
if (!response.ok) {
throw new Error(`IA API error: ${response.status}`);
}
const data = await response.json();
return data.summary;
}
// services/activityService.ts
export async function findUserActivities(userId: string, limit: number, page: number) {
const offset = (page - 1) * limit;
// Imagine um SELECT simples no banco interno
const rows = await db("activities")
.where({ user_id: userId })
.orderBy("created_at", "desc")
.limit(limit)
.offset(offset);
const [{ count }] = await db("activities")
.where({ user_id: userId })
.count("* as count");
return { rows, total: Number(count) };
}
E o endpoint (versão “ruim”):
// controllers/reportController.ts
import { summarizeWithAI } from "../services/aiClient";
import { findUserActivities } from "../services/activityService";
export async function getUserReportsHandler(req, reply) {
const userId = req.params.userId;
const page = Number(req.query.page ?? 1);
const limit = Number(req.query.limit ?? 5);
const { rows, total } = await findUserActivities(userId, limit, page);
const reports = [];
for (const activity of rows) {
const summary = await summarizeWithAI(activity.raw_text); // <-- SEQUENCIAL
reports.push({
activityId: activity.id,
createdAt: activity.created_at,
summary,
});
}
return reply.send({
reports,
total,
totalPages: Math.ceil(total / limit),
});
}
Se:
- findUserActivities demora ~200–400ms;
- cada summarizeWithAI leva ~2s;
- e você traz 5 atividades por página…
Você tem: ~2s × 5 + overhead ≈ 10–12s.
Se subir para 10 atividades, facilmente chega perto de 20–25s. Com mais chamadas internas, outra API, ou IA mais lenta, esse “budget” de tempo explode.
4. Primeiro passo: concorrer chamadas de IA com Promise.all
A primeira melhoria óbvia é fazer as chamadas de IA em paralelo:
// controllers/reportController.ts
export async function getUserReportsHandler(req, reply) {
const userId = req.params.userId;
const page = Number(req.query.page ?? 1);
const limit = Number(req.query.limit ?? 5);
const { rows, total } = await findUserActivities(userId, limit, page);
// Dispara todas as chamadas ao mesmo tempo
const summaries = await Promise.all(
rows.map((activity) => summarizeWithAI(activity.raw_text))
);
const reports = rows.map((activity, idx) => ({
activityId: activity.id,
createdAt: activity.created_at,
summary: summaries[idx],
}));
return reply.send({
reports,
total,
totalPages: Math.ceil(total / limit),
});
}
Se cada chamada de IA ainda leva ~2s, mas você faz 5 em paralelo, o tempo total passa a ser ~2–3s para todas (mais o tempo de DB). Em projetos reais, é comum ver endpoints caindo de ~40s para ~8s apenas com esse tipo de abordagem — desde que a API externa tolere essa concorrência e seu backend esteja configurado para isso.
Mas não é só sair aumentando concorrência.
5. Controlando a concorrência: limitar “fan-out” para não derrubar nada
Se você simplesmente fizer Promise.all em 50 itens, pode:
- estourar limite de conexões HTTP;
- violar rate limits da API de IA;
- ou saturar algum recurso interno.
Por isso, um padrão muito útil é um “limiter” de concorrência: em vez de disparar todas as promessas de uma vez, você impõe um máximo de tarefas rodando simultaneamente.
Um exemplo simples de “pool” genérico:
// utils/runWithConcurrency.ts
export async function runWithConcurrency<T>(
items: T[],
concurrency: number,
worker: (item: T) => Promise<void>
): Promise<void> {
const queue = [...items];
const workers: Promise<void>[] = [];
async function runWorker() {
while (queue.length > 0) {
const item = queue.shift();
if (!item) return;
await worker(item);
}
}
const workerCount = Math.min(concurrency, items.length);
for (let i = 0; i < workerCount; i++) {
workers.push(runWorker());
}
await Promise.all(workers);
}
Aplicando no endpoint:
import { runWithConcurrency } from "../utils/runWithConcurrency";
export async function getUserReportsHandler(req, reply) {
const userId = req.params.userId;
const page = Number(req.query.page ?? 1);
const limit = Number(req.query.limit ?? 5);
const concurrency = 3; // por exemplo
const { rows, total } = await findUserActivities(userId, limit, page);
const reports: any[] = [];
await runWithConcurrency(rows, concurrency, async (activity) => {
const summary = await summarizeWithAI(activity.raw_text);
reports.push({
activityId: activity.id,
createdAt: activity.created_at,
summary,
});
});
// Se a ordenação for importante, você pode ordenar depois por createdAt ou id
reports.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1));
return reply.send({
reports,
total,
totalPages: Math.ceil(total / limit),
});
}
Agora você:
- ganha concorrência (várias chamadas acontecendo ao mesmo tempo);
- mas mantém controle (no máximo concurrency chamadas de IA em paralelo).
Esse é o padrão que, na prática, costuma trazer ganhos do tipo “40 segundos → ~8 segundos” de forma relativamente simples.
6. Separando ingestão externa de leitura paginada
Outro ponto que ajuda muito (e que quase sempre aparece em sistemas com IA + integrações externas):
- Ingestão/sincronização de dados externos,
- Consulta/leitura desses dados via endpoints com paginação.
Em vez de chamar o sistema externo toda vez que você muda de página, é mais saudável:
Ter um serviço de importação que:
- acessa o sistema externo,
- normaliza e salva no seu banco,
- aplica concorrência e paralelismo onde fizer sentido,
- garante idempotência (reprocessar o mesmo input deixa o sistema no mesmo estado, por exemplo, sem duplicar dados).
Ter um endpoint paginado “limpo”, que:
- só lê do seu banco local,
- ordena e pagina normalmente,
- não fala mais com o sistema externo.
Essa separação reduz a latência percebida pelo usuário e facilita muito o raciocínio sobre performance e cache.
Em código, ficaria algo como:
// services/externalIngestService.ts
export class ExternalIngestService {
constructor(private readonly externalApiClient: ExternalApiClient) {}
public async syncUserData(userId: string): Promise<void> {
const batch = await this.externalApiClient.fetchFullHistory({ userId });
// Concorrência controlada para normalizar e salvar:
await runWithConcurrency(batch.items, 5, async (externalItem) => {
const fingerprint = this.computeFingerprint(userId, externalItem);
const alreadyExists = await this.existsByFingerprint(fingerprint);
if (alreadyExists) return;
const normalized = this.normalizeExternalItem(externalItem);
await this.saveNormalized(userId, fingerprint, normalized);
});
}
// ... computeFingerprint, existsByFingerprint, normalizeExternalItem, saveNormalized
}
E o endpoint paginado:
// controllers/reportController.ts
export async function getUserReportsHandler(req, reply) {
const userId = req.params.userId;
const page = Number(req.query.page ?? 1);
const limit = Number(req.query.limit ?? 5);
// 1) Tenta sincronizar com o sistema externo (mas não falha o endpoint se der erro)
try {
await externalIngestService.syncUserData(userId);
} catch (err) {
req.log.error({ err, userId }, "External sync failed");
}
// 2) Lê somente do seu banco (já sincronizado)
const { rows, total } = await reportsRepository.findByUserPaginated(userId, page, limit);
return reply.send({
reports: rows,
total,
totalPages: Math.ceil(total / limit),
});
}
O ganho aqui não é só de latência, mas de arquitetura:
- você consegue melhorar paralelismo/concorrência na ingestão sem tocar nos endpoints de leitura;
- pode mover a ingestão para jobs assíncronos, filas, cron, etc.;
- e o endpoint em si fica muito mais previsível.
7. Onde entra paralelismo “de verdade”?
Até agora, tudo era concorrência I/O-bound em um processo único (Node usando event loop e non-blocking IO).
Mas em cenários de IA você pode ter situações CPU-bound:
- pós-processamento pesado de respostas (análises estatísticas, embeddings locais, etc.);
- parsing de arquivos grandes;
- compressão, criptografia, etc.
Nesses casos, colocar tudo em Promise.all não ajuda, porque o gargalo é CPU. A solução é usar paralelismo real:
- Worker Threads em Node;
- múltiplos processos (cluster, containers);
- ou até serviços separados.
A literatura de programação concorrente fala bastante sobre estruturas de dados lock-free, algoritmos para multiprocessadores e modelos de memória, especialmente em livros como The Art of Multiprocessor Programming (Herlihy & Shavit) e Java Concurrency in Practice (Goetz et al.).
Mesmo que você esteja em Node, entender esses conceitos ajuda a decidir quando vale a pena paralelizar CPU e quando só precisa de concorrência I/O.
8. Medindo: mais importante do que “achar” que ficou rápido
Antes e depois de qualquer refatoração de concorrência/paralelismo, meça:
- latência média e p95/p99 do endpoint (por página, por tipo de usuário, etc.);
distribuição de tempo de:
- consulta ao banco;
- chamadas de IA;
- outros serviços externos.
Ferramentas simples que já ajudam muito:
- logs com startTime/endTime por etapa;
- métricas em Prometheus / Grafana;
- tracing distribuído (OpenTelemetry) quando seu sistema começa a crescer.
Só com métricas você consegue dizer:
“estava ~40 segundos em média, agora está ~8 segundos no p95 para páginas de 5 itens”.
Sem isso, é fácil cair em autoengano: paralelizar um trecho que não era gargalo ou, pior, aumentar a concorrência de forma que piore tudo (mais context switching, mais carga em APIs externas, mais erros intermitentes).
9. Checklist prático para endpoints com IA
Se você está construindo ou refatorando um endpoint que chama IA externa, aqui vai um checklist resumido:
Identifique o tipo de carga
A lógica é majoritariamente I/O-bound (HTTP, DB, IA)?
→ foque em concorrência (Promises, pools, limiters).Possui trechos CPU-bound?
→ considere worker threads ou processos separados.
Evite loops sequenciais com IO lento
- Troque loops
forcomawaitsequencial porPromise.allou por um pool de concorrência.
Limite a concorrência
- Use um parâmetro de
concurrencyconfigurável (por ambiente). - Respeite rate limits e capacidades do provedor de IA.
Separe ingestão externa de leitura
- Tenha serviços de ingestão/sync que materializam dados no seu banco.
- Deixe os endpoints de leitura o mais “puros” possível, apenas paginando e ordenando.
Use idempotência ao integrar sistemas externos
- Idempotência aqui = chamar a mesma operação com o mesmo input N vezes e o sistema permanecer no mesmo estado depois da primeira execução.
- Crie uma forma de fingerprint (hash) dos dados externos por entidade/usuário.
- Antes de inserir, verifique se aquele fingerprint já existe.
- Assim, reprocessar o mesmo lote de dados não altera mais nada (efeito prático: você evita duplicidade de registros).
Meça sempre
- Logue tempos por etapa e por página.
- Monitore p95/p99, não só a média.
Estude fundamentos
- Conceitos de processos, threads, sincronização, deadlocks, starvation (livros de SO).
- Estruturas de dados e algoritmos concorrentes (Herlihy & Shavit).
- Impacto da “revolução da concorrência” no software moderno (Herb Sutter).
10. Conclusão
A onda atual de IA deixou muita gente com a impressão de que o grande diferencial está apenas em “saber falar com modelos”. Na prática, quando você vai para produção, os velhos fundamentos de computação voltam com força total:
- entender o modelo de concorrência da linguagem/plataforma (Node, JVM, etc.);
- saber a diferença entre tarefas I/O-bound e CPU-bound;
- estruturar pipelines com fan-out/fan-in, limites de concorrência, ingestão assíncrona e leitura paginada.
No nosso cenário hipotético, sair de um loop sequencial para um desenho concorrente e bem limitado reduziu latência na casa de 40s para ≈8s — sem trocar de stack, sem usar mágica, apenas aplicando princípios de Sistemas Operacionais e Programação Concorrente que já existem há décadas.
IA muda o que você pode fazer com os dados.
Concorrência, paralelismo e arquitetura ainda determinam se isso vai rodar bem em produção.
Referências recomendadas
- Andrew S. Tanenbaum, Herbert Bos. Modern Operating Systems. 4ª ed.
- Maurice Herlihy, Nir Shavit. The Art of Multiprocessor Programming.
- Brian Goetz et al. Java Concurrency in Practice.
- Herb Sutter. “The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software”. Dr. Dobb’s Journal, 2005.
- Martin Kleppmann. Designing Data-Intensive Applications (capítulos sobre sistemas distribuídos, concorrência e tolerância a falhas).
Top comments (0)