The first time my Telegram bot got real users, I made the obvious mistake.
I thought moderation was a classification problem.
Pick a strong model. Write a careful prompt. Maybe add a few examples. Send every message to GPT-5 or Claude Opus 4.6 and let the model decide whether it was spam.
That worked right up until the bot got hit with actual spam.
Not a gentle trickle. A burst.
Crypto links. Copy-pasted promos. Weird Unicode junk. Fresh accounts joining in clusters. The same message repeated across multiple groups like someone had automated annoyance itself.
That was the moment I learned the real thing:
Telegram moderation is not a prompt problem. It’s a workflow problem.
If you treat every message like a fresh LLM task, your bot loses in two places at once:
- it wastes model calls on obvious junk
- it burns through Telegram action limits trying to clean up the mess
The fix was building moderation like an agent pipeline: cheap filters first, selective model calls second, and one shared outbound queue for deletes, warnings, restrictions, and retries.
If you’re building bots for Telegram, Discord, Slack, or any always-on automation, this pattern matters a lot more than prompt tweaks.
The thing that actually broke first
At first my bot looked smart.
Message comes in. Model decides spam or not spam. Bot calls Telegram methods like:
deleteMessagesendMessagerestrictChatMemberanswerCallbackQuery
In my head, this was elegant.
One message in, one decision out.
In production, one spam message could trigger several outbound API calls.
That’s the part people underestimate.
Telegram rate limits are not just about sending newsletters or broadcasts. They affect moderation too. The practical numbers most bot builders run into are roughly:
- about
1 message/secondper chat - about
20 messages/minuteper group - about
30 messages/secondglobally
And methods like sendMessage, editMessageText, deleteMessage, and answerCallbackQuery all compete for that budget.
So spam doesn’t just create inbound load.
It creates outbound moderation load.
That was my first real lesson: the bot wasn’t only struggling to identify spam. It was spending its own oxygen trying to remove it.
What I got wrong: I kept tuning prompts
Of course I did the normal LLM engineer thing.
I tweaked prompts.
Maybe GPT-5 was overthinking obvious junk. Maybe Claude Opus 4.6 was too forgiving. Maybe a smaller model would be better for first-pass triage. Maybe the moderation instruction needed stronger wording.
Wrong layer.
The first thing that failed was not model quality.
It was control flow.
When Telegram rate-limits you, you get an HTTP 429 with a retry_after value:
{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 5",
"parameters": {"retry_after": 5}
}
That response is basically Telegram telling you how your bot should be designed.
If every handler retries independently, your bot becomes its own denial-of-service attack.
One delete retries.
One warning retries.
One callback answer retries.
One restriction retries.
Now your moderation system is fighting Telegram instead of fighting spam.
That’s when I stopped thinking “better prompt” and started thinking queue.
The fix: treat moderation like an agent workflow
I rebuilt the bot like a small operations system.
Not “message in, LLM out.”
More like:
- deterministic checks
- selective model escalation
- centralized outbound actions
- retry handling in one place
If you use n8n, Make, Zapier, OpenClaw, or your own background workers, this pattern will feel familiar.
Stage 1: deterministic junk filters
Before any model saw a message, I ran cheap checks.
Things like:
- message length thresholds
- normalized text hash for duplicate detection
- link density
- mention count
- repeated emoji or Unicode noise
- invite-link patterns
- join-event rules
- first-message heuristics
- account-age heuristics if available
Most spam is lazy.
If a message is 95% links, repeated three times, and matches the same normalized body as your last seven deletions, you do not need GPT-5 to meditate on intent.
Here’s a simple Python sketch:
import re
import hashlib
INVITE_PATTERNS = [
r"t\.me/",
r"telegram\.me/",
r"joinchat/",
]
URL_RE = re.compile(r"https?://|www\.")
MENTION_RE = re.compile(r"@\w+")
EMOJI_SPAM_RE = re.compile(r"([\U0001F300-\U0001FAFF])\1{4,}")
def normalize_text(text: str) -> str:
text = text.lower().strip()
text = re.sub(r"\s+", " ", text)
return text
def text_hash(text: str) -> str:
return hashlib.sha256(normalize_text(text).encode()).hexdigest()
def cheap_spam_score(text: str) -> dict:
normalized = normalize_text(text)
url_count = len(URL_RE.findall(normalized))
mention_count = len(MENTION_RE.findall(normalized))
invite_match = any(re.search(p, normalized) for p in INVITE_PATTERNS)
emoji_noise = bool(EMOJI_SPAM_RE.search(normalized))
score = 0
if len(normalized) > 800:
score += 1
if url_count >= 2:
score += 2
if mention_count >= 5:
score += 2
if invite_match:
score += 3
if emoji_noise:
score += 2
return {
"score": score,
"hash": text_hash(normalized),
"url_count": url_count,
"mention_count": mention_count,
"invite_match": invite_match,
"emoji_noise": emoji_noise,
}
This kind of pre-filtering cuts traffic more than people expect.
Not because the rules are genius.
Because spam is repetitive.
Stage 2: only call models for ambiguous messages
Once I added deterministic filtering, I stopped sending every message to a premium model.
That changed both latency and cost.
My opinion here is pretty strong:
Fallback stacks are better than one “best” model.
For moderation, I’d rather do this:
- rules catch obvious junk
- a fast model handles gray-area text
- a stronger model reviews low-confidence or high-impact cases
- a human queue handles appeals and weird edge cases
That’s not overengineering. That’s how you keep always-on systems stable.
Pseudo-flow:
def classify_message(msg):
cheap = cheap_spam_score(msg.text)
if cheap["score"] >= 5:
return {"label": "spam", "source": "rules", "confidence": 0.99}
if is_recent_duplicate(cheap["hash"]):
return {"label": "spam", "source": "duplicate_hash", "confidence": 0.99}
fast_result = call_fast_model(msg.text)
if fast_result["confidence"] >= 0.9:
return fast_result
if fast_result["label"] == "spam" or msg.is_high_risk:
return call_stronger_model(msg.text)
return fast_result
If you’re paying per token, there’s constant pressure to avoid second-pass review.
That’s one reason flat-cost inference changes how you design these systems. You can afford to build a safer escalation path instead of pretending one prompt should do everything.
For bots that run 24/7, that matters.
The real fix was one shared outbound queue
This was the part that actually made the bot reliable.
Every action that touched Telegram went through one queue.
Everything:
- deletes
- warning messages
- edits
- callback answers
- user restrictions
- ban/unban actions
Not five retry loops.
One queue.
Here’s the idea in Python:
import time
import queue
outbound = queue.PriorityQueue()
def enqueue_action(priority, action):
outbound.put((priority, time.time(), action))
def worker(telegram_client):
while True:
priority, _, action = outbound.get()
try:
telegram_client.execute(action)
except Telegram429 as e:
retry_after = e.retry_after or 1
time.sleep(retry_after)
enqueue_action(priority, action)
except Exception as e:
log_error(action, e)
finally:
outbound.task_done()
And if you want something more production-friendly, use Redis, SQS, RabbitMQ, or Postgres-backed jobs instead of an in-memory queue.
The architecture matters more than the library.
The key idea is that deleteMessage, sendMessage, editMessageText, and answerCallbackQuery should compete in one place under one rate policy.
That stopped the stampede.
The bug that wasn’t a bug: Telegram wasn’t delivering everything
Then I hit a second issue that looked like “the model is missing spam.”
It wasn’t.
The bot simply wasn’t seeing all messages.
Privacy mode will absolutely waste your time
Telegram bots in groups can have limited visibility depending on privacy mode and admin rights.
That means you can spend hours debugging “low recall” when the model never received the message in the first place.
Before you benchmark GPT-5, Claude Opus 4.6, Grok, Qwen, or anything else, verify these first:
- the bot is an admin where needed
- privacy mode is configured correctly
- you’re subscribed to the update types you expect
- your webhook handler isn’t the bottleneck
For webhooks, max_connections matters too:
curl -X POST "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
-d "url=https://example.com/webhook" \
-d "max_connections=100"
That won’t fix your moderation logic.
But it will stop you from blaming the model for delivery problems.
Single prompt vs workflow: what survives real traffic?
Here’s the clean version.
| Approach | What happens in real traffic |
|---|---|
| Naive single-prompt moderation | One LLM call per message. Fast to ship. Falls apart under bursts because it ignores queueing, duplicate suppression, and Telegram action limits. |
| Workflow-based moderation pipeline | Cheap preprocessing before model calls. Centralized rate limiting and retry_after handling. Mixes deterministic rules, model escalation, and tool actions. |
| Telegram paid broadcast | Improves outbound throughput. Does not solve inbound spam detection, duplicate suppression, or moderation decisioning. |
That last row matters because people see Telegram’s paid broadcast option and think they’ve solved scale.
They haven’t.
Paid broadcast changes outbound throughput economics.
It does not decide whether a message is spam. It does not dedupe repeated junk. It does not stop your retries from colliding.
You still need architecture.
Why this changes how I think about inference cost
If your bot handles a few private messages per hour, a single moderation prompt is fine.
Seriously. Don’t build a tiny air traffic control tower for a hobby bot.
But group moderation is different.
Always-on bots are different.
Agent workflows are different.
Once the bot runs continuously, your pricing model starts shaping your system design.
Per-token billing pushes people into bad tradeoffs:
- skipping second-pass review
- avoiding richer classification prompts
- removing safety checks from edge cases
- under-instrumenting background jobs
- treating every extra model call like a financial threat
That’s how people end up with brittle systems that look cheap until traffic spikes.
This is exactly where Standard Compute is interesting for developers building automations and bots.
It gives you an OpenAI-compatible API with flat monthly pricing instead of per-token billing. So if your moderation flow needs:
- a cheap first-pass classifier
- a stronger fallback model
- retries inside background workers
- nonstop agent traffic from n8n, Make, Zapier, OpenClaw, or custom code
…you can design for reliability instead of constantly trimming model calls to save pennies.
That’s the real appeal for agent-style workflows. Not “unlimited” as a slogan. More like: you can finally build the sane pipeline instead of the cheapest possible one.
What I’d build first if I were shipping this again
If you’re building a Telegram moderation bot this week, here’s the order I’d use.
1) Confirm visibility first
Make sure the bot can actually see the messages you expect.
Check:
- admin permissions
- privacy mode
- webhook delivery
- update subscriptions
2) Add deterministic preprocessing
Start with the obvious wins:
- duplicates
- normalized text hashes
- links
- mentions
- repeated Unicode noise
- invite patterns
- first-message heuristics
3) Call models selectively
Do not spend GPT-5 or Claude on obvious junk.
Use a cheap path first. Escalate only when needed.
4) Centralize outbound actions
One queue for:
- deletes
- warnings
- edits
- restrictions
- callback responses
5) Respect retry_after
Treat HTTP 429 as a control signal, not an exception to smash through.
6) Instrument everything
Track:
- inbound message rate
- model calls per message
- queue depth
- duplicate hit rate
- 429 frequency
- moderation latency
- false positive review rate
A simple metrics sketch:
from collections import Counter
metrics = Counter()
def record_message_received():
metrics["messages_received"] += 1
def record_model_call(model_name):
metrics[f"model_call:{model_name}"] += 1
def record_429(method_name):
metrics[f"telegram_429:{method_name}"] += 1
def record_queue_depth(depth):
metrics["last_queue_depth"] = depth
In production, send that to Prometheus, Grafana, Datadog, OpenTelemetry, whatever you already use.
Just don’t run blind.
The main takeaway
The comforting part is that Telegram moderation gets easier once you stop pretending it’s a pure AI problem.
It’s an operations problem with an AI component.
That’s actually good news.
Prompts are slippery.
Workflows are debuggable.
When my bot finally stopped getting wrecked by spam, it wasn’t because I found a magic moderation prompt.
It was because I treated moderation like an agent system with:
- gates
- queues
- retries
- model escalation
- shared rate limits
Less romantic than “one brilliant prompt fixed everything.”
Much more useful.
If you’re building bots or automations that never sleep, that mindset shift is the difference between a demo and a system that survives contact with real users.
Top comments (0)