I Need to Moderate Content but Can't Add a 1-Second API Call
Compare moderation approaches by measured quality and latency, including rules, hosted APIs, live LLMs, and compiled classifiers.
TL;DR
Content moderation forces a project-specific trade-off between policy quality, tail latency, review load, and cost. Sparkient's controlled synthetic compiled-model run reports 0.900 macro F1, 91.5% accuracy, and 41ms batch-average time per item. It does not measure per-request p95. Local latency and production suitability must be verified on the target hardware and content.
The Constraint
You need content moderation. Trust & Safety, regulatory compliance, brand protection — the reasons don't matter. What matters is the constraint:
Your UX cannot tolerate a 1-second delay between "submit" and "posted."
You're building a chat app, a social feed, a marketplace listing flow, or a comment section. Users expect their content to appear instantly. Every millisecond of delay after they hit "submit" feels like friction. At 500ms, it's noticeable. At 1 second, it's annoying. At 2 seconds, users start rage-tapping.
And yet, you need to check every piece of content before it's visible to other users. Pre-publish moderation — not post-publish cleanup.
Let's look at every option, with real numbers.
Option 1: Keyword Filters
How it works
Maintain a list of blocked words and regex patterns. Check each submission against the list.
import re
BLOCKED_PATTERNS = [
r'\b(spam|scam|fake)\b',
r'\b(buy\s+followers)\b',
r'(https?://\S+){3,}', # 3+ links
# ... 500 more patterns
]
compiled = [re.compile(p, re.IGNORECASE) for p in BLOCKED_PATTERNS]
def moderate_keywords(text: str) -> str:
for pattern in compiled:
if pattern.search(text):
return "reject"
return "approve"Performance
| Metric | Value | |--------|-------| | Latency | <1ms | | Accuracy | ~55% F1 | | False positive rate | 15-25% | | False negative rate | 30-40% | | Setup effort | Low | | Maintenance | High (constant rule updates) |
The problem
Keyword filters can't handle context. "I'm going to kill this presentation" isn't a threat. "Check my profile for more details 😉" doesn't contain any blocked words but is clearly spam. Obfuscation bypasses them trivially: "fr33 m0ney" passes any keyword filter looking for "free money."
The false positive rate is equally painful. Legitimate users get their content rejected because they used a word that appears in a different context. "This product is trash" is a valid negative review, not a policy violation.
Verdict: Useful as a first layer for obvious cases, but completely inadequate as a standalone solution.
Option 2: Perspective API (Google Jigsaw)
How it works
Google's ML-based toxicity scoring API. Send text, get scores for toxicity, severe toxicity, insult, profanity, identity attack, and threat.
from googleapiclient import discovery
client = discovery.build("commentanalyzer", "v1alpha1",
developerKey="YOUR_API_KEY")
def moderate_perspective(text: str) -> str:
response = client.comments().analyze(body={
"comment": {"text": text},
"requestedAttributes": {
"TOXICITY": {},
"SEVERE_TOXICITY": {},
"SPAM": {}
}
}).execute()
toxicity = response["attributeScores"]["TOXICITY"]["summaryScore"]["value"]
if toxicity > 0.8:
return "reject"
elif toxicity > 0.5:
return "review"
return "approve"Performance
| Metric | Value | |--------|-------| | Latency | Measure the current endpoint from your deployment region | | Quality | Evaluate against your policy and content distribution | | False positive rate | Measure per policy class | | False negative rate | Measure per policy class | | Setup effort | Low | | Cost and limits | Verify current provider terms |
The problem
Perspective API is designed for toxicity detection, not general content moderation. It doesn't understand your specific policies. "Buy my product at discount-pills.com" isn't toxic — it scores low on toxicity — but it's spam that violates your content policy.
Its end-to-end latency may or may not fit the application; include the client network and downstream processing in the measurement.
Verdict: Good for toxicity screening. Not sufficient for custom content policies, spam detection, or brand-specific moderation rules.
Option 3: OpenAI Moderation API
How it works
OpenAI's free moderation endpoint classifies content across categories: hate, harassment, self-harm, sexual, violence, and more.
import openai
async def moderate_openai(text: str) -> str:
response = await openai.moderations.create(input=text)
result = response.results[0]
if result.flagged:
# Check which categories were flagged
categories = result.categories
if categories.violence or categories.hate:
return "reject"
return "review"
return "approve"Performance
| Metric | Value | |--------|-------| | Latency | Measure the current endpoint from your deployment region | | Quality | Evaluate against the provider taxonomy and your own policy | | False positive rate | Measure per safety class | | False negative rate | Measure per safety class | | Setup effort | Very low | | Cost and limits | Verify current provider terms |
The problem
Like Perspective API, OpenAI Moderation covers predefined safety categories. If your moderation policy includes spam detection, self-promotion, misinformation, or platform-specific rules (e.g., "no recruitment posts in this community"), the moderation API doesn't cover those.
Measure the endpoint inside the real form or message flow. A provider-level request time does not include all client, application, and downstream work.
Verdict: Excellent free option for safety-category moderation. Not customizable to platform-specific policies. Latency is borderline.
Option 4: GPT-4o Custom Moderation
How it works
Call GPT-4o (or GPT-4o-mini) with a custom prompt that describes your specific moderation policy.
async def moderate_gpt4o(text: str) -> str:
response = await openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": """You are a content moderator.
Classify as: approve, review, or reject.
Policy:
- Reject: hate speech, threats, explicit content, scams
- Review: self-promotion, borderline language, new account spam patterns
- Approve: legitimate content, opinions, questions
Respond with only the classification."""},
{"role": "user", "content": text}
],
temperature=0,
max_tokens=10
)
return response.choices[0].message.content.strip()Performance
| Metric | Value | |--------|-------| | Latency | Measure the chosen model, prompt, and provider | | Accuracy | Evaluate on representative project content | | False positive rate | Measure per policy class | | False negative rate | Measure per policy class | | Setup effort | Low (prompt engineering) | | Cost | Current provider price × actual input/output tokens |
The problem
This may be the simplest high-quality option, but only a project evaluation can establish accuracy. Measure the exact prompt's full-path latency against the application's budget.
For a chat app or content feed, trace the time from submission to visible outcome and compare it with the product's own interaction budget.
The cost can also become material; calculate it from current provider pricing and observed tokens rather than request count alone.
Verdict: Keep the live LLM if it meets the project's quality, latency, and cost requirements; otherwise test a compiled candidate on the same cases.
Option 5: Compiled ONNX Classifier
How it works
Use an LLM to generate or label examples for a lightweight classifier offline. The compiled stage targets sub-100ms; cloud decisions can optionally escalate at low confidence, and the actual escalation rate is workload-specific.
import httpx
async def moderate_compiled(text: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.sparkient.ai/api/v1/decide",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"decision_type": "content-moderation",
"input": {"text": text, "user_id": "user_123"}
}
)
return response.json()
# The response includes decision, confidence, measured latency_ms, and stage.For latency-critical applications, run the model locally:
from sparkient_edge import EdgePredictor
predictor = EdgePredictor.from_bundle("moderation.zip")
result = predictor.predict({"text": "Check out this amazing deal!"})
# EdgeDecision includes the decision, confidence, stage, reason codes, and class probabilities.Performance
| Metric | Cloud API | Edge (Local) | |--------|-----------|-------------| | Compiled-stage evidence | 41ms batch-average time per item in the controlled synthetic run | Benchmark per-request latency on target hardware | | Quality proof | 0.900 macro F1; 91.5% accuracy in that benchmark | Same exported model; verify local preprocessing and outputs | | Quality | Same exported model; verify per-class metrics | Same exported model; verify locally | | Cost model | Sparkient plan credits | Subscription plus local infrastructure |
The trade-off
The public benchmark establishes 0.900 macro F1 for the compiled content-moderation model; it does not establish a universal gap to GPT-4o. If optional escalation is enabled, evaluate the combined pipeline separately because its quality, latency, and credits differ from classifier-only results.
Verdict: A credible candidate for latency-sensitive text moderation when it passes the project's per-class safety thresholds and full-path load test.
The Full Comparison
| Approach | Runtime profile | Policy control | What to verify | |----------|-----------------|----------------|----------------| | Keyword filters | Usually sub-millisecond | Manual rules | Evasion and per-class errors | | General moderation API | Provider dependent | Provider taxonomy | Coverage, latency, and current terms | | Custom live LLM | Model and prompt dependent | Prompt-defined | Quality, tokens, latency, and reliability | | Sparkient cloud | 41ms batch-average time per item in the controlled synthetic run | Trained on your policy | Full-path quality, per-request latency, escalation, and credits | | Sparkient edge | Local hardware dependent | Same exported policy model | Local latency, packaging, and operations |
No option is automatically best. Use representative content and weight false approvals and false rejections according to the platform's risk.
Replacing an OpenAI Moderation Call
Here is an instrumentation pattern for comparing two paths in the same chat application. The code deliberately returns measured latency rather than assuming provider or Sparkient timings.
Before: OpenAI Moderation + GPT-4o Fallback
async def moderate_message(message: str) -> dict:
# Step 1: Provider moderation call — time this request
mod_result = await openai.moderations.create(input=message)
if mod_result.results[0].flagged:
return {"action": "reject", "reason": "safety_violation"}
# Step 2: Custom policy call — time this request separately
response = await openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Check for: spam, self-promotion, solicitation..."},
{"role": "user", "content": message}
]
)
classification = response.choices[0].message.content.strip()
return {"action": classification}After: Compiled Classifier
import httpx
async def moderate_message(message: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.sparkient.ai/api/v1/decide",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"decision_type": "content-moderation",
"input": {"text": message}
}
)
result = response.json()
return {
"action": result["decision"],
"confidence": result["confidence"],
"latency_ms": result["latency_ms"]
}The first approach chains two remote calls; the second tests one purpose-built policy model. Compare both on the same messages, with the same policy labels, and report end-to-end p50/p95/p99, false approvals, false rejections, escalation, and current usage cost. Do not infer a speed multiplier from the published moderation benchmark.
When to Combine Approaches
For maximum safety coverage, you can layer approaches:
async def moderate_layered(message: str) -> dict:
# Layer 1: Keyword blocklist (instant, catches obvious violations)
if matches_blocklist(message):
return {"action": "reject", "source": "blocklist"}
# Layer 2: Compiled classifier (fast, handles nuance)
result = await sparkient_classify(message)
# Layer 3: Only for low-confidence edge cases
if result["confidence"] < 0.7:
result = await llm_escalate(message)
return resultThis gives you:
- Instant rejection of known-bad content (blocklist)
- Measured classifier decisions above the configured threshold
- Optional LLM decisions for low-confidence cases when enabled
FAQ
Q: Are 0.900 macro F1 and 91.5% accuracy enough for content moderation? Neither aggregate metric answers that question or can be converted into universal false-positive and false-negative rates. Set per-class safety thresholds, compare a representative held-out set, and evaluate any human-review or optional-escalation path separately.
Q: What about content in languages other than English? The text encoder used in the compiled model is trained on English. For multilingual moderation, you'd need to train with multilingual examples or use a multilingual base model. Sparkient's training pipeline supports this — provide multilingual training examples and the model will learn to handle them. For full multilingual coverage, an LLM-based approach currently has an advantage.
Q: Can I moderate images and videos with this approach? This article covers text moderation only. For image and video moderation, you'll need dedicated vision models (Google Cloud Vision, AWS Rekognition, or custom models). A common pattern is to moderate text with a compiled classifier and images with a vision API, combining the results. The text moderation is usually the latency bottleneck that this approach solves.
Q: What happens if a harmful message gets through (false negative)? No moderation system is 100% accurate. Provide a report path, review selected outcomes, add corrected examples deliberately, and evaluate the next model version before deployment. Optional LLM escalation may change false negatives, but measure that effect and retain a human path for consequential reports.
Evaluate moderation against your own quality and latency budget. Start free — 5,000 credits, no credit card required.
Ready to get started?
Start with 5,000 free credits and 250 decisions. No credit card required.
Start Free