Escalando a moderação de conteúdo
Uma configuração de moderação que funciona com 1.000 mensagens por dia não se sustenta em um milhão: os limites de taxa começam a pesar, a latência por chamada se acumula e os custos disparam. Este guia mostra como integrar a moderação da Discuse a pipelines de alto volume — quando bloquear, quando verificar em segundo plano, o que armazenar em cache e como contornar picos de carga.
O que deixa de funcionar quando a moderação escala?
Cada problema de escala tem uma causa específica e uma correção específica:
| Desafio | Impacto | Solução |
|---|---|---|
| Crescimento do volume | Limites de taxa da API, aumento de custos | Processamento assíncrono, cache |
| Requisitos de latência | Experiência ruim para o usuário | Pré-moderação, filas |
| Gestão de custos | Restrições de orçamento | Roteamento inteligente, cache |
| Consistência | Aplicação irregular das regras | Configuração centralizada |
| Tratamento de picos | Degradação do serviço | Escalonamento automático, filas |
Qual padrão de arquitetura devo usar?
Padrão 1: Pré-moderação síncrona
Ideal para: Conteúdo de baixo volume e alto impacto (pagamentos, jurídico)
User Input → API Call → Moderation → Decision → Response
↓
(Blocking call)
// Simple synchronous flow
async function createPost(content) {
// Check moderation before saving
const result = await checkModeration(content);
if (result.has_violations) {
throw new ModerationError(result.message);
}
// Safe to publish
return await savePost(content);
}
Prós: Simples, feedback imediato Contras: Aumenta a latência, limita a taxa de processamento
Padrão 2: Pós-moderação assíncrona
Ideal para: Conteúdo de alto volume e menor impacto (comentários, mensagens)
User Input → Save (Pending) → Publish → Background Check → Action
↓
┌────────┴────────┐
↓ ↓
Safe Violation
↓ ↓
Keep Remove
// Async flow with queue
async function createPost(content) {
// Save immediately with pending status
const post = await savePost(content, { status: 'pending' });
// Queue for async moderation
await moderationQueue.add('check-content', {
postId: post.id,
content: content
});
return post;
}
// Background worker
moderationQueue.process('check-content', async (job) => {
const result = await checkModeration(job.data.content);
if (result.has_violations) {
await removePost(job.data.postId);
await notifyUser(job.data.postId, 'content_removed');
} else {
await updatePost(job.data.postId, { status: 'approved' });
}
});
Prós: Não bloqueia, lida bem com volume Contras: Conteúdo prejudicial fica visível por um curto período
Padrão 3: Moderação em camadas
Ideal para: Uma abordagem equilibrada com otimização de custos
User Input → Quick Check → High Confidence? ──Yes──► Auto-decision
│
No
↓
Full Analysis ──► Decision
async function tieredModeration(content) {
// Tier 1: Fast local checks (regex, blocklists)
const localResult = await quickLocalCheck(content);
if (localResult.definiteViolation) {
return { action: 'block', source: 'local' };
}
// Tier 2: Cached API results
const cacheKey = hashContent(content);
const cached = await cache.get(cacheKey);
if (cached) {
return { action: cached.action, source: 'cache' };
}
// Tier 3: Full API check
const apiResult = await checkModeration(content);
await cache.set(cacheKey, apiResult, TTL);
return { action: determineAction(apiResult), source: 'api' };
}
Padrão 4: Distribuição com agregação
Ideal para: Vários tipos de verificação com escalabilidade independente
┌──► Text Check ──┐
│ │
User Input ──► Router ──► Image Check ──► Aggregator ──► Decision
│ │
└──► Link Check ──┘
async function parallelModeration(content) {
const checks = [];
if (content.text) {
checks.push(checkText(content.text));
}
if (content.images?.length) {
checks.push(checkImages(content.images));
}
if (content.links?.length) {
checks.push(checkLinks(content.links));
}
// Run all checks in parallel
const results = await Promise.all(checks);
// Aggregate results
return aggregateResults(results);
}
function aggregateResults(results) {
// Most severe result wins
const hasViolation = results.some(r => r.has_violations);
const maxScore = Math.max(...results.map(r => r.max_score || 0));
return {
has_violations: hasViolation,
max_score: maxScore,
details: results
};
}
Estratégias de cache
Cache com base no conteúdo
Armazene em cache os resultados para conteúdos idênticos:
const cache = new Redis();
async function checkWithCache(content) {
const hash = crypto.createHash('sha256')
.update(content.text || '')
.update(JSON.stringify(content.images || []))
.digest('hex');
// Check cache first
const cached = await cache.get(`mod:${hash}`);
if (cached) {
return { ...JSON.parse(cached), cached: true };
}
// API call
const result = await callModerationAPI(content);
// Cache for 24 hours
await cache.setex(`mod:${hash}`, 86400, JSON.stringify(result));
return { ...result, cached: false };
}
Cache com base no usuário
Armazene em cache as decisões para infratores reincidentes:
async function checkUserHistory(userId) {
const recentViolations = await cache.get(`violations:${userId}`);
if (recentViolations > 5) {
// Flag for enhanced scrutiny
return { enhancedModeration: true };
}
return { enhancedModeration: false };
}
async function recordViolation(userId) {
await cache.incr(`violations:${userId}`);
await cache.expire(`violations:${userId}`, 86400 * 7); // 7 days
}
Gerenciamento de filas
Filas de prioridade
Trate diferentes tipos de conteúdo com prioridades diferentes:
const queues = {
critical: new Queue('moderation-critical'), // Reports, appeals
high: new Queue('moderation-high'), // Public posts
normal: new Queue('moderation-normal'), // Comments, DMs
low: new Queue('moderation-low') // Profile updates
};
async function queueForModeration(content, priority = 'normal') {
const queue = queues[priority] || queues.normal;
return queue.add('check', {
contentId: content.id,
type: content.type,
data: content
}, {
priority: getPriorityNumber(priority),
timeout: getTimeout(priority)
});
}
Respeitando limites de taxa
Cada chave de API tem um limite de requisições por minuto; ultrapassá-lo faz com que a API rejeite a chamada com um erro de "rate limit exceeded". Aplique limitação do seu lado para permanecer abaixo desse limite e evitar gastar cota com chamadas redundantes. Defina o teto por minuto do limitador para corresponder ao RPM configurado da sua chave:
const Bottleneck = require('bottleneck');
const limiter = new Bottleneck({
maxConcurrent: 50, // Cap parallel requests
minTime: 20, // Min 20ms between requests
reservoir: YOUR_KEY_RPM, // Match your API key's per-minute limit
reservoirRefreshAmount: YOUR_KEY_RPM,
reservoirRefreshInterval: 60 * 1000 // Refill each minute
});
async function rateLimitedCheck(content) {
return limiter.schedule(() => checkModeration(content));
}
A cota é separada do limite de taxa: cada chamada conta para a franquia mensal de requisições de API do seu plano, e o objeto usage da resposta informa api_requests_used, api_requests_limit e api_requests_remaining para que você possa acompanhar a margem disponível.
Filas de mensagens mortas
Trate tentativas de moderação que falharam:
moderationQueue.process('check-content', async (job) => {
try {
const result = await checkModeration(job.data.content);
await applyDecision(job.data.contentId, result);
} catch (error) {
if (job.attemptsMade < 3) {
throw error; // Retry
}
// Move to dead letter queue after 3 attempts
await deadLetterQueue.add('failed-moderation', {
...job.data,
error: error.message,
attempts: job.attemptsMade
});
// Apply safe default (hold for review)
await holdForManualReview(job.data.contentId);
}
});
Otimização de custos
Roteamento inteligente
Chame APIs pagas apenas quando necessário:
async function smartModeration(content) {
// Step 1: Free local checks
const localResult = runLocalFilters(content);
if (localResult.isDefinitelySpam) {
return { action: 'block', cost: 0 };
}
// Step 2: Check cache (free)
const cached = await getCachedResult(content);
if (cached) {
return { action: cached.action, cost: 0 };
}
// Step 3: Risk-based API call
const riskScore = calculateRiskScore(content, localResult);
if (riskScore < 0.2) {
// Low risk: approve without API call
return { action: 'approve', cost: 0 };
}
// Step 4: API call for uncertain content
const apiResult = await checkModeration(content);
return {
action: determineAction(apiResult),
cost: calculateApiCost(content)
};
}
Controle de concorrência
O endpoint /api/v2/check avalia um envio por chamada. Para processar em "lote", agrupe os itens no seu worker e distribua-os simultaneamente dentro de um pool limitado, em vez de esperar uma única solicitação com vários itens. Uma verificação que inclui várias URLs de mídia continua sendo uma única chamada que cobre todas elas — uma chamada com muitas imagens sai mais barata do que várias chamadas com uma única imagem.
// Bounded concurrency: many items, capped parallel calls.
async function processBatch(items, concurrency = 10) {
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const slice = items.slice(i, i + concurrency);
const settled = await Promise.allSettled(
slice.map(item => checkModeration(item.content))
);
results.push(...settled);
}
return results;
}
Uma única solicitação pode conter até 10 URLs de imagens, 5 URLs de GIFs, 3 URLs de vídeos, 5 URLs de documentos e 20 links, além de texto com até 10.000 caracteres — portanto, consolide a mídia de um envio em uma única chamada em vez de dividi-la.
Monitoramento e alertas
Painel de métricas principais
const metrics = {
// Volume metrics
total_requests: new Counter('moderation_requests_total'),
requests_per_second: new Gauge('moderation_rps'),
// Latency metrics
latency_p50: new Histogram('moderation_latency_p50'),
latency_p99: new Histogram('moderation_latency_p99'),
// Queue metrics
queue_depth: new Gauge('moderation_queue_depth'),
processing_time: new Histogram('moderation_processing_time'),
// Cost metrics
api_calls: new Counter('moderation_api_calls'),
estimated_cost: new Counter('moderation_estimated_cost'),
// Accuracy metrics
violations_detected: new Counter('moderation_violations'),
false_positives: new Counter('moderation_false_positives')
};
Regras de alerta
# Alert on queue backup
- alert: ModerationQueueBackup
expr: moderation_queue_depth > 10000
for: 5m
labels:
severity: warning
annotations:
summary: Moderation queue backing up
# Alert on high latency
- alert: ModerationLatencyHigh
expr: moderation_latency_p99 > 5000
for: 2m
labels:
severity: critical
annotations:
summary: Moderation latency exceeding 5 seconds
# Alert on high error rate
- alert: ModerationErrorRate
expr: rate(moderation_errors_total[5m]) / rate(moderation_requests_total[5m]) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: Moderation error rate above 1%
Escalabilidade da infraestrutura
Escalabilidade horizontal
# Kubernetes HPA for moderation workers
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: moderation-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: moderation-worker
minReplicas: 3
maxReplicas: 50
metrics:
- type: External
external:
metric:
name: queue_depth
target:
type: AverageValue
averageValue: 100
Redução da latência para usuários globais
A API da Discuse é acessada por uma única URL base, https://api.discuse.com. Para manter a latência baixa no tráfego global, execute seus workers de moderação perto dos seus usuários e deixe que eles chamem a API, em vez de chamá-la de forma síncrona a partir de um fluxo de requisição voltado ao usuário em uma região distante. Combine isso com os padrões de pós-moderação assíncrona e cache acima para que uma ida e volta lenta nunca bloqueie um usuário.
Resumo das melhores práticas
- Use cache de forma agressiva: conteúdos idênticos retornam um resultado em cache. A API também elimina duplicidades de conteúdos idênticos por projeto em uma janela curta e retorna
cached: true. - Use filas: processamento assíncrono para grandes volumes.
- Implemente camadas: local → cache → API.
- Monitore tudo: profundidade da fila, latência e uso em relação à sua cota.
- Planeje-se para falhas: filas de mensagens não entregues e alternativas seguras.
- Escale horizontalmente: mais workers, não máquinas maiores.
- Otimize custos: roteamento inteligente, menos chamadas redundantes e mídia consolidada por solicitação.
Próximos passos
- Guia de moderação de conteúdo com AI - Entenda a moderação com AI
- Configuração de limites - Ajuste fino da detecção
- Guia de início rápido - Implementação básica