7 Bugs Ocultos de Produção que Agentes de IA Criam (E Como Pegá-los Antes do Crash)
Seu agente de IA acabou de construir uma feature em 45 segundos. O código compila. Os testes passam. O PR tá limpo. Você faz deploy em produção numa sexta à noite confiante.
Sábado de manhã, seu banco tá a 100% de CPU, o cluster Redis não responde, e três clientes reportaram que tão vendo os dados dos outros. O engenheiro de plantão tá encarando código gerado por IA que nunca viu, e nada nos logs faz sentido porque a IA não botou logging de erro decente.
Isso não é cenário hipotético. Tá acontecendo todo dia na indústria. Um estudo de 2025 da Endor Labs descobriu que 62% do código gerado por IA contém fraquezas de segurança ou falhas de design. Mas o problema mais traiçoeiro não são os bugs óbvios: são os ocultos. O código que parece correto, passa nos unit tests, sobrevive ao code review e detona em condições reais que ninguém pensou em testar.
Esse guia diseca os 7 padrões de bugs ocultos mais perigosos que agentes de IA produzem consistentemente. Pra cada padrão, cobrimos por que a IA gera isso, como detectar antes de ir pra produção e o fix testado em batalha. Não são riscos teóricos: são padrões extraídos de centenas de incidentes de produção em empresas rodando workflows de dev assistido por IA em 2025-2026.
Padrão 1: O Cache Stampede
O Que a IA Gera
Quando você pede pro agente adicionar caching, ele gera lógica cache-aside de livro-texto:
async function getProduct(id: string): Promise<Product> { const cached = await redis.get(`product:${id}`); if (cached) { return JSON.parse(cached); } const product = await db.query('SELECT * FROM products WHERE id = $1', [id]); await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 3600); return product; }
Funciona perfeito isolado. Todos os testes passam.
Por Que Detona
Sob carga de produção, quando uma cache key popular expira, centenas de requests concorrentes fazem miss no cache simultaneamente. Todos batem no banco com a mesma query. CPU do banco sobe a 100%, queries começam a dar timeout e a cascata de falhas derruba serviços que nem são relacionados.
Isso é um cache stampede (thundering herd). A IA produz isso porque os dados de treino são dominados por exemplos de tutorial que assumem ambientes single-thread e baixo tráfico.
Estratégia de Detecção
it('deve lidar com cache misses concorrentes sem stampede', async () => { await redis.del('product:popular-item'); const requests = Array.from({ length: 100 }, () => getProduct('popular-item') ); const dbQuerySpy = vi.spyOn(db, 'query'); await Promise.all(requests); expect(dbQuerySpy).toHaveBeenCalledTimes(1); });
O Fix de Produção
import { Mutex } from 'async-mutex'; const locks = new Map<string, Mutex>(); async function getProduct(id: string): Promise<Product> { const cacheKey = `product:${id}`; const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } if (!locks.has(cacheKey)) { locks.set(cacheKey, new Mutex()); } const mutex = locks.get(cacheKey)!; return mutex.runExclusive(async () => { const rechecked = await redis.get(cacheKey); if (rechecked) { return JSON.parse(rechecked); } const product = await db.query( 'SELECT * FROM products WHERE id = $1', [id] ); await redis.set(cacheKey, JSON.stringify(product), 'EX', 3600); return product; }); }
O insight chave: double-checked locking. O primeiro request pega o lock, busca no banco e preenche o cache. Todos os outros requests esperam pelo lock, encontram o cache pronto e retornam sem bater no banco.
Padrão 2: Esgotamento do Connection Pool
O Que a IA Gera
Agentes adoram async/await mas consistentemente produzem código que vaza conexões:
async function processOrder(orderId: string) { const client = await pool.connect(); const order = await client.query( 'SELECT * FROM orders WHERE id = $1', [orderId] ); const inventory = await client.query( 'SELECT * FROM inventory WHERE product_id = $1', [order.rows[0].product_id] ); await client.query( 'UPDATE inventory SET quantity = quantity - $1 WHERE product_id = $2', [order.rows[0].quantity, order.rows[0].product_id] ); client.release(); return { success: true }; }
Por Que Detona
Se qualquer query der erro — violação de constraint, timeout, deadlock — client.release() nunca executa. A conexão vaza. Depois de vazamentos suficientes, pool.connect() trava indefinidamente porque todas as conexões tão presas em handlers abandonados. O app inteiro para sem nenhum log de erro.
Estratégia de Detecção
// Monitoramento de métricas do pool em produção setInterval(() => { const { totalCount, idleCount, waitingCount } = pool; logger.info('pool_metrics', { total: totalCount, idle: idleCount, waiting: waitingCount, active: totalCount - idleCount, }); if (waitingCount > 5) { logger.warn('pool_pressure', { message: 'Connection pool sob pressão', waiting: waitingCount, }); } }, 10_000);
O Fix de Produção
async function processOrder(orderId: string) { const client = await pool.connect(); try { await client.query('BEGIN'); const order = await client.query( 'SELECT * FROM orders WHERE id = $1', [orderId] ); const inventory = await client.query( 'SELECT * FROM inventory WHERE product_id = $1', [order.rows[0].product_id] ); await client.query( 'UPDATE inventory SET quantity = quantity - $1 WHERE product_id = $2', [order.rows[0].quantity, order.rows[0].product_id] ); await client.query('COMMIT'); return { success: true }; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); // SEMPRE libera, com ou sem erro } }
O padrão try/catch/finally garante liberação da conexão aconteça o que acontecer. É o padrão que agentes omitem com mais frequência.
Padrão 3: Corrupção Silenciosa de Dados
O Que a IA Gera
Lidando com dados de API, a IA confia cegamente na entrada externa:
app.post('/api/users/:id/profile', async (req, res) => { const { name, email, role } = req.body; await db.query( 'UPDATE users SET name = $1, email = $2, role = $3 WHERE id = $4', [name, email, role, req.params.id] ); res.json({ success: true }); });
Por Que Detona
Esse endpoint permite que qualquer usuário autenticado mude seu role pra admin. A IA desestruturou role do request body porque o schema tem uma coluna role, e o pattern matching da IA conectou os dois sem considerar autorização.
Pior: name e email aceitam qualquer string. Dados "salvam" com sucesso mas corrompem sistemas downstream.
Estratégia de Detecção
import { z } from 'zod'; const UpdateProfileSchema = z.object({ name: z.string().min(1).max(100).trim(), email: z.string().email().max(254).toLowerCase(), // 'role' NÃO tá no schema }); // Teste de integração: verificar que campos proibidos são rejeitados it('não deve permitir escalação de role via update de perfil', async () => { const res = await request(app) .post('/api/users/user-1/profile') .send({ name: 'Hacker', email: '[email protected]', role: 'admin' }) .set('Authorization', `Bearer ${userToken}`); const user = await db.query('SELECT role FROM users WHERE id = $1', ['user-1']); expect(user.rows[0].role).toBe('member'); // NÃO 'admin' });
O Fix de Produção
import { z } from 'zod'; const UpdateProfileSchema = z.object({ name: z.string().min(1).max(100).trim(), email: z.string().email().max(254).toLowerCase(), // 'role' NÃO tá no schema }); app.post('/api/users/:id/profile', async (req, res) => { const parsed = UpdateProfileSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: 'Validation failed', issues: parsed.error.issues, }); } const { name, email } = parsed.data; if (req.params.id !== req.user.id) { return res.status(403).json({ error: 'Forbidden' }); } await db.query( 'UPDATE users SET name = $1, email = $2 WHERE id = $3', [name, email, req.params.id] ); res.json({ success: true }); });
Três camadas de defesa: validação de schema (remove role), check de autorização (só perfil próprio), e tipos de dados restritos.
Padrão 4: Race Conditions Não Tratadas
O Que a IA Gera
Implementando funcionalidade de "curtir":
async function toggleLike(userId: string, postId: string) { const existing = await db.query( 'SELECT id FROM likes WHERE user_id = $1 AND post_id = $2', [userId, postId] ); if (existing.rows.length > 0) { await db.query('DELETE FROM likes WHERE id = $1', [existing.rows[0].id]); await db.query( 'UPDATE posts SET like_count = like_count - 1 WHERE id = $1', [postId] ); return { liked: false }; } else { await db.query( 'INSERT INTO likes (user_id, post_id) VALUES ($1, $2)', [userId, postId] ); await db.query( 'UPDATE posts SET like_count = like_count + 1 WHERE id = $1', [postId] ); return { liked: true }; } }
Por Que Detona
Dê dois toques rápidos no botão de curtir com conexão ruim. Ambos os requests veem que não há curtida existente (o primeiro INSERT não commitou). Ambos inserem. like_count incrementa 2. A race condition é invisível em testes porque rodam sequencialmente.
Estratégia de Detecção
it('deve lidar com toggles de curtida concorrentes corretamente', async () => { const results = await Promise.all([ toggleLike('user-1', 'post-1'), toggleLike('user-1', 'post-1'), ]); const likes = await db.query( 'SELECT COUNT(*) FROM likes WHERE user_id = $1 AND post_id = $2', ['user-1', 'post-1'] ); // Deve ser exatamente 0 ou 1, nunca 2 expect(Number(likes.rows[0].count)).toBeLessThanOrEqual(1); });
O Fix de Produção
async function toggleLike(userId: string, postId: string) { return await db.transaction(async (tx) => { const existing = await tx.query( `SELECT id FROM likes WHERE user_id = $1 AND post_id = $2 FOR UPDATE`, // ← Lock a nível de linha [userId, postId] ); if (existing.rows.length > 0) { await tx.query('DELETE FROM likes WHERE id = $1', [existing.rows[0].id]); await tx.query( 'UPDATE posts SET like_count = like_count - 1 WHERE id = $1', [postId] ); return { liked: false }; } else { await tx.query( `INSERT INTO likes (user_id, post_id) VALUES ($1, $2) ON CONFLICT (user_id, post_id) DO NOTHING`, [userId, postId] ); await tx.query( 'UPDATE posts SET like_count = like_count + 1 WHERE id = $1', [postId] ); return { liked: true }; } }); }
FOR UPDATE serializa operações concorrentes e ON CONFLICT DO NOTHING serve de rede de segurança. Agentes de IA quase nunca geram FOR UPDATE.
Padrão 5: A Tempestade de Retries
O Que a IA Gera
Quando você pede "torna essa chamada mais resiliente":
async function callPaymentAPI(data: PaymentRequest): Promise<PaymentResult> { const MAX_RETRIES = 3; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const response = await fetch('https://api.payment.com/charge', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) { throw new Error(`Payment API error: ${response.status}`); } return await response.json(); } catch (error) { if (attempt === MAX_RETRIES - 1) throw error; } } throw new Error('Unreachable'); }
Por Que Detona
Com a API de pagamento degradada, cada request dispara até 3 chamadas rápidas pro serviço que já tá sofrendo. 1.000 usuários viram 3.000 requests. Pior: a IA deu retry num POST /charge. Se o primeiro já passou mas a resposta atrasou, o retry cria cobrança duplicada.
Estratégia de Detecção
it('deve implementar backoff exponencial, não retry imediato', async () => { let callTimestamps: number[] = []; vi.spyOn(global, 'fetch').mockImplementation(async () => { callTimestamps.push(Date.now()); throw new Error('Service unavailable'); }); await expect(callPaymentAPI(mockData)).rejects.toThrow(); // Verificar delays exponenciais entre retries for (let i = 1; i < callTimestamps.length; i++) { const delay = callTimestamps[i] - callTimestamps[i - 1]; expect(delay).toBeGreaterThan(500 * Math.pow(2, i - 1)); } });
O Fix de Produção
async function callPaymentAPI(data: PaymentRequest): Promise<PaymentResult> { const idempotencyKey = crypto.randomUUID(); const MAX_RETRIES = 3; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); const response = await fetch('https://api.payment.com/charge', { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, signal: controller.signal, }); clearTimeout(timeout); if (response.status === 429 || response.status >= 500) { throw new RetryableError(response.status); } if (!response.ok) { throw new NonRetryableError(response.status); } return await response.json(); } catch (error) { if (error instanceof NonRetryableError) throw error; if (attempt === MAX_RETRIES - 1) throw error; const baseDelay = 1000 * Math.pow(2, attempt); const jitter = Math.random() * 500; await new Promise(resolve => setTimeout(resolve, baseDelay + jitter) ); } } throw new Error('Unreachable'); }
Quatro adições críticas: chaves de idempotência, backoff exponencial com jitter, classificação de erros retryable vs non-retryable, e timeouts explícitos.
Padrão 6: O Memory Leak Lento
O Que a IA Gera
Construindo handlers de WebSocket ou eventos:
class NotificationService { private listeners: Map<string, Set<(data: any) => void>> = new Map(); subscribe(userId: string, callback: (data: any) => void) { if (!this.listeners.has(userId)) { this.listeners.set(userId, new Set()); } this.listeners.get(userId)!.add(callback); } notify(userId: string, data: any) { this.listeners.get(userId)?.forEach(cb => cb(data)); } } wss.on('connection', (ws, req) => { const userId = extractUserId(req); const callback = (data: any) => { ws.send(JSON.stringify(data)); }; notificationService.subscribe(userId, callback); ws.on('message', (msg) => { /* handle */ }); });
Por Que Detona
Não tem unsubscribe na desconexão. Cada conexão adiciona um callback, nenhuma remove. Depois de uma semana, o Map acumula milhões de callbacks mortos. Memória cresce linearmente até OOM.
Estratégia de Detecção
it('deve limpar listeners na desconexão', async () => { const ws = new MockWebSocket(); simulateConnection(ws, 'user-1'); const listenersBefore = notificationService.getListenerCount('user-1'); expect(listenersBefore).toBe(1); ws.emit('close'); const listenersAfter = notificationService.getListenerCount('user-1'); expect(listenersAfter).toBe(0); }); // Monitoramento em produção setInterval(() => { let total = 0; notificationService.listeners.forEach((set) => { total += set.size; }); logger.info('listener_count', { total }); }, 60_000);
O Fix de Produção
wss.on('connection', (ws, req) => { const userId = extractUserId(req); const callback = (data: any) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(data)); } }; notificationService.subscribe(userId, callback); ws.on('close', () => { notificationService.unsubscribe(userId, callback); }); ws.on('error', () => { notificationService.unsubscribe(userId, callback); }); ws.on('message', (msg) => { /* handle */ }); });
A IA sempre implementa subscribe mas esquece do unsubscribe. Dados de treino raramente mostram código de limpeza.
Padrão 7: O Vazamento de Contexto Auth
O Que a IA Gera
function authMiddleware(req: Request, res: Response, next: NextFunction) { const token = req.headers.authorization?.split(' ')[1]; const decoded = jwt.verify(token!, process.env.JWT_SECRET!); req.user = decoded as AuthUser; next(); } app.get('/api/orders', authMiddleware, async (req, res) => { const orders = await orderService.getOrders(req.query.userId as string); res.json(orders); });
Por Que Detona
O middleware valida o JWT e seta req.user. Mas o controller passa req.query.userId (do query string) pro serviço em vez de req.user.id (do token verificado). Qualquer usuário autenticado acessa orders de qualquer outro mudando o parâmetro. É uma vulnerabilidade IDOR.
Estratégia de Detecção
it('não deve permitir acesso a orders de outros usuários', async () => { const tokenA = generateToken({ id: 'user-a', role: 'member' }); const res = await request(app) .get('/api/orders?userId=user-b') .set('Authorization', `Bearer ${tokenA}`); const orders = res.body; orders.forEach((order: any) => { expect(order.user_id).toBe('user-a'); }); });
O Fix de Produção
app.get('/api/orders', authMiddleware, async (req, res) => { // ✅ Usa identidade verificada do JWT const orders = await orderService.getOrders(req.user.id); res.json(orders); });
A regra é simples: nunca use identidade fornecida pelo usuário quando você tem uma verificada.
O Meta-Padrão: Por Que a IA Produz Esses Bugs
Os sete padrões compartilham a mesma causa raiz: agentes de IA otimizam pro happy path.
Distribuição de Dados de Treino vs. Realidade do Código:
Dados de Treino: Código de Produção:
───────────────── ───────────────────
Happy path: 85% Happy path: 20%
Error handling: 10% Error handling: 35%
Edge cases: 3% Edge cases: 25%
Concorrência: 1% Concorrência: 10%
Cleanup: 1% Cleanup: 10%
A IA gera lindamente os 20% que tratam operações normais, mas ignora ou trata superficialmente os 80% que fazem código ser production-ready.
Checklist de Detecção
| Categoria | Pergunta | Se "Não" → |
|---|---|---|
| Concorrência | Funciona com 1000 requests simultâneos? | Adicionar locking / FOR UPDATE / idempotência |
| Conexões | São liberadas num bloco finally? | Adicionar try/finally |
| Validação | Toda entrada externa é validada? | Adicionar schemas Zod |
| Auth | Usa identidade verificada, não fornecida? | Trocar req.query por req.user |
| Retries | Usa backoff exponencial + chaves de idempotência? | Trocar loops de retry ingênuos |
| Cleanup | Listeners, timers e subscriptions são limpos? | Adicionar handlers close/error |
| Erros | Têm contexto suficiente pra debug? | Adicionar logging estruturado |
| Consistência | Um crash entre dois writes deixa dados inconsistentes? | Wrappear em transaction |
Construindo sua Camada Defensiva
A defesa mais eficaz não é revisar cada linha de código gerado por IA. É construir infraestrutura que pegue esses padrões automaticamente.
1. Testes de Integração Obrigatórios pra Concorrência
Não teste só se o código funciona. Teste se funciona sob carga concorrente. Toda operação de escrita precisa de um teste com Promise.all() que verifique corretude sob race conditions.
2. Monitoramento de Connection Pool
Adicione métricas de pool em tempo real (conexões ativas, em espera, idle) ao seu stack de observabilidade. Alerte quando waiting > 5. Isso pega connection leaks antes de virarem outages.
3. Validação de Schema em Cada Fronteira
Use uma lib de validação (Zod, Valibot, ArkType) em cada ponto onde dados cruzam uma fronteira de confiança: endpoints de API, consumers de filas, handlers de webhooks, respostas de APIs de terceiros. Nunca confie que a forma dos dados é a que você espera.
4. Arquitetura Auth-by-Default
Estruture seu codebase pra que autenticação e autorização sejam impossíveis de pular acidentalmente. Serviços devem receber um parâmetro AuthContext verificado, não user IDs crus. Se um método de serviço não recebe um AuthContext, deve ser erro de compilação.
5. Regras de Análise Estática
Crie regras custom de ESLint ou Biome que flaguem anti-padrões conhecidos:
pool.connect()sem umrelease()correspondente num blocofinally- Loops de retry com
fetch()sem delay addEventListener()/subscribe()sem cleanup correspondente- Queries de banco que usam
req.queryoureq.paramspra identidade
Conclusão
Agentes de IA são multiplicadores de produtividade com disciplina. São multiplicadores de dívida sem ela.
Os 7 padrões desse guia não são edge cases. São os modos de falha mais comuns, mais danosos e mais previsíveis do código gerado por IA. Todos são preveníveis com infraestrutura: testes de concorrência, monitoramento de conexões, validação de schema, arquitetura auth-by-default e análise estática.
Seu agente escreve o primeiro rascunho. Sua disciplina de engenharia escreve o final. O código que parece correto é o código mais perigoso de todos.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit