Back

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

CategoriaPerguntaSe "Não" →
ConcorrênciaFunciona com 1000 requests simultâneos?Adicionar locking / FOR UPDATE / idempotência
ConexõesSão liberadas num bloco finally?Adicionar try/finally
ValidaçãoToda entrada externa é validada?Adicionar schemas Zod
AuthUsa identidade verificada, não fornecida?Trocar req.query por req.user
RetriesUsa backoff exponencial + chaves de idempotência?Trocar loops de retry ingênuos
CleanupListeners, timers e subscriptions são limpos?Adicionar handlers close/error
ErrosTêm contexto suficiente pra debug?Adicionar logging estruturado
ConsistênciaUm 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 um release() correspondente num bloco finally
  • Loops de retry com fetch() sem delay
  • addEventListener() / subscribe() sem cleanup correspondente
  • Queries de banco que usam req.query ou req.params pra 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.

AIDebuggingProductionSoftware EngineeringCode QualityBest Practices

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit