Escalar la moderación de contenido
Una configuración de moderación que funciona con 1.000 mensajes al día se rompe al llegar al millón: los límites de frecuencia empiezan a afectar, la latencia por llamada se acumula y los costes se disparan. Esta guía muestra cómo integrar la moderación de Discuse en canalizaciones de alto rendimiento: cuándo bloquear, cuándo comprobar en segundo plano, qué almacenar en caché y cómo desviar la carga en los picos.
¿Qué falla cuando la moderación escala?
Cada problema de escalado tiene una causa concreta y una solución específica:
| Desafío | Impacto | Solución |
|---|---|---|
| Crecimiento del volumen | Límites de tasa de la API, aumento de costes | Procesamiento asíncrono, caché |
| Requisitos de latencia | Mala experiencia de usuario | Premoderación, colas |
| Gestión de costes | Restricciones presupuestarias | Enrutamiento inteligente, caché |
| Consistencia | Aplicación desigual | Configuración centralizada |
| Gestión de picos | Degradación del servicio | Autoescalado, colas |
¿Qué patrón de arquitectura debería usar?
Patrón 1: Premoderación síncrona
Ideal para: Contenido de bajo volumen y alto riesgo (pagos, legal)
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);
}
Ventajas: Sencillo, respuesta inmediata Desventajas: Añade latencia, limita el rendimiento
Patrón 2: Posmoderación asíncrona
Ideal para: Contenido de alto volumen y menor riesgo (comentarios, mensajes)
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' });
}
});
Ventajas: No bloqueante, soporta mucho volumen Desventajas: El contenido dañino puede quedar visible brevemente
Patrón 3: Moderación por niveles
Ideal para: Un enfoque equilibrado con optimización de costes
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' };
}
Patrón 4: Distribución con agregación
Ideal para: Varios tipos de comprobación con escalado independiente
┌──► 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
};
}
Estrategias de caché
Caché basada en el contenido
Almacena en caché los resultados para contenido idéntico:
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 };
}
Caché basada en el usuario
Almacena en caché las decisiones para 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
}
Gestión de colas
Colas de prioridad
Gestiona distintos tipos de contenido con diferentes prioridades:
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)
});
}
Respeto de los límites de tasa
Cada clave de API tiene un límite de solicitudes por minuto; si lo superas, la API rechazará la llamada con un error de "rate limit exceeded". Aplica control de velocidad en tu lado para mantenerte por debajo de ese límite y evitar consumir cuota en llamadas redundantes. Configura el límite por minuto del limitador para que coincida con las RPM configuradas para tu clave:
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));
}
La cuota es independiente del límite de tasa: cada llamada cuenta para el cupo mensual de solicitudes de API de tu plan, y el objeto usage de la respuesta informa api_requests_used, api_requests_limit y api_requests_remaining para que puedas controlar el margen disponible.
Colas de mensajes fallidos
Gestiona los intentos de moderación fallidos:
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);
}
});
Optimización de costos
Enrutamiento inteligente
Llama a las APIs de pago solo cuando sea necesario:
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)
};
}
Control de concurrencia
El endpoint /api/v2/check puntúa un envío por llamada. Para "procesar en lote", agrupa los elementos en tu worker y distribúyelos en paralelo con un pool limitado, en lugar de esperar una única solicitud con varios elementos. Una comprobación que incluye varias URL de medios cuenta como una sola llamada que las cubre todas: una llamada con muchas imágenes resulta más barata que muchas llamadas con una sola imagen.
// 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;
}
Una sola solicitud puede incluir hasta 10 URL de imágenes, 5 URL de GIF, 3 URL de vídeos, 5 URL de documentos y 20 enlaces, además de texto de hasta 10.000 caracteres; por tanto, consolida los medios de un envío en una sola llamada en lugar de dividirlos.
Monitoreo y alertas
Panel de métricas clave
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')
};
Reglas 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%
Escalado de la infraestructura
Escalado 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
Reducción de la latencia para usuarios de todo el mundo
Se accede a la API de Discuse desde una única URL base, https://api.discuse.com. Para mantener baja la latencia del tráfico global, ejecuta tus workers de moderación cerca de tus usuarios y deja que ellos llamen a la API, en lugar de invocarla de forma síncrona desde una ruta de solicitud orientada al usuario en una región lejana. Combina esto con los patrones de posmoderación asíncrona y almacenamiento en caché anteriores para que un recorrido de ida y vuelta lento nunca bloquee a un usuario.
Resumen de mejores prácticas
- Almacena en caché de forma agresiva: el contenido idéntico devuelve un resultado almacenado en caché. La API también deduplica el contenido idéntico por proyecto dentro de una ventana breve y devuelve
cached: true. - Usa colas: procesamiento asíncrono para grandes volúmenes.
- Implementa niveles: local → caché → API.
- Supervísalo todo: profundidad de la cola, latencia y uso respecto a tu cuota.
- Prepárate para fallos: colas de mensajes no entregados y alternativas seguras.
- Escala horizontalmente: más trabajadores, no máquinas más grandes.
- Optimiza el coste: enrutamiento inteligente, menos llamadas redundantes y medios consolidados por solicitud.
Próximos pasos
- Guía de moderación de contenido con AI - Comprender la moderación con AI
- Configuración de umbrales - Ajustar la detección
- Guía de inicio rápido - Implementación básica