Worum es geht
Eine Pipeline erzeugt Meldungen nicht gleichmäßig, sondern in Schüben: Ein neuer Feed liefert viele Einträge, ein Ereignis löst mehrere Alarme aus, oder ein Fehler wiederholt sich in einer Schleife. Die Telegram-Bot-API begrenzt, wie schnell ein Bot Nachrichten senden darf, insbesondere an einen einzelnen Chat. Wird das Limit überschritten, antwortet der Server mit dem Status 429 und einer Wartezeit.
Die genauen Zahlen ändern sich und stehen in der offiziellen Dokumentation der Bot-API. Für den Aufbau der Pipeline sind sie nicht entscheidend, wichtig ist ein Versand, der Limits respektiert, statt gegen sie anzurennen.
Was bei einer Überschreitung passiert
Die Antwort mit Status 429 enthält im Body ein Feld retry_after, das die Wartezeit in Sekunden angibt. Wer diese Angabe ignoriert und sofort erneut sendet, verlängert im schlimmsten Fall die Sperre. Halte dich deshalb an den gemeldeten Wert.
Bausteine eines robusten Versands
- Warteschlange: Meldungen werden nicht direkt gesendet, sondern in eine Queue gelegt. Ein einzelner Sender arbeitet sie ab.
- Ratenbegrenzung: Der Sender hält einen Mindestabstand zwischen Nachrichten ein.
- Retry mit Backoff: Bei Fehlern wartet er länger, bei 429 genau die geforderte Zeit.
- Zusammenfassen: Viele ähnliche Meldungen werden zu einer gebündelt.
- Priorisierung: Alarme überholen Routine.
Ein einfacher Sender mit Queue
import asyncio
import logging
import random
import aiohttp
log = logging.getLogger("telegram")
async def sender(queue: asyncio.Queue, token: str, chat_id: str,
min_gap: float = 1.1) -> None:
url = f"https://api.telegram.org/bot{token}/sendMessage"
async with aiohttp.ClientSession() as session:
while True:
text = await queue.get()
attempt = 0
while True:
async with session.post(url, json={"chat_id": chat_id,
"text": text}) as resp:
if resp.status == 200:
break
if resp.status == 429:
data = await resp.json()
wait = data.get("parameters", {}).get("retry_after", 5)
else:
attempt += 1
wait = min(60, 2 ** attempt) + random.random()
log.warning("Status %s, warte %.1f s", resp.status, wait)
await asyncio.sleep(wait)
queue.task_done()
await asyncio.sleep(min_gap)
Der Wert min_gap ist ein Beispiel, das du an die Dokumentation und an deinen Chat anpasst. Der Token gehört in eine Umgebungsdatei und nicht in den Code.
Backoff mit Jitter
Ohne Zufallsanteil würden mehrere Sender nach einem Fehler gleichzeitig wieder anfragen. Der kleine Zufallswert im Beispiel (random.random()) verteilt die Versuche. Die Wartezeit wächst exponentiell, wird aber nach oben begrenzt, damit sie nicht ins Unendliche läuft.
Coalescing: Meldungen bündeln
Fünfzig Meldungen mit gleichem Anlass sind für Menschen nutzlos. Sammle sie kurz, etwa zehn Sekunden, und sende eine einzige Nachricht mit Anzahl und den wichtigsten Zeilen.
def coalesce(messages: list[str], limit: int = 5) -> str:
head = messages[:limit]
rest = len(messages) - len(head)
text = "\n".join(head)
if rest > 0:
text += f"\n... und {rest} weitere"
return text
Das entlastet das Limit und den Leser gleichermaßen. Bewahre die vollständigen Details in der Datenbank auf, damit nichts verloren geht.
Priorisierung
Nicht jede Meldung ist gleich dringend. Mit einer asyncio.PriorityQueue überholen Alarme die Routine. Achte darauf, dass Routine nicht dauerhaft verhungert: Der Heartbeat sollte in einem eigenen Kanal oder mit garantierter Zustellung laufen.
Häufige Fehler
retry_afterignoriert und im Schleifentakt erneut gesendet.- Mehrere Prozesse senden parallel mit demselben Bot, sodass das Limit gemeinsam überschritten wird.
- Fehlgeschlagene Meldungen gehen verloren, weil es keine Wiederholung gibt.
- Nachrichten werden ungekürzt gesendet und scheitern an der Längenbegrenzung.
Wann sich das lohnt (und wann nicht)
Der Aufwand lohnt sich, wenn Schübe vorkommen: mehrere Kanäle, viele Feeds oder Webhooks Dritter, deren Menge du nicht kontrollierst. Dann ist eine Queue mit Backoff und Bündelung die stabile Lösung.
Bei wenigen Meldungen am Tag an einen einzelnen Chat reicht eine kurze Pause zwischen den Nachrichten. Ein Message-Broker oder aufwendiges Routing wäre hier Überbau. Für reine Alarme ohne Antwortfunktion ist außerdem ein Dienst wie ntfy einen Blick wert.