A publishing API times out. The tempting response is to retry. But a timeout tells you that the client did not receive a response; it does not prove that the server rejected the post.
Here is a small, deliberately in-memory Python sketch of the decision boundary. It is not a production publisher. Its job is to make one rule testable: uncertain delivery must not silently return to the send queue.
def deliver(item, send):
if item["state"] != "approved":
return
item["state"] = "sending"
try:
receipt = send()
except TimeoutError:
item["state"] = "uncertain"
return
if receipt.get("chat_id") != item["target"]:
item["state"] = "uncertain"
return
if not receipt.get("message_id"):
item["state"] = "uncertain"
return
item["receipt"] = receipt
item["state"] = "published"
calls = []
def timeout_after_request():
calls.append("sent")
raise TimeoutError()
item = {"state": "approved", "target": -100123}
deliver(item, timeout_after_request)
deliver(item, timeout_after_request)
assert item["state"] == "uncertain"
assert len(calls) == 1
wrong = {"state": "approved", "target": -100123}
deliver(wrong, lambda: {"chat_id": -100999, "message_id": 7})
assert wrong["state"] == "uncertain"
ok = {"state": "approved", "target": -100123}
deliver(ok, lambda: {"chat_id": -100123, "message_id": 8})
assert ok["state"] == "published"
What this tests
The second call does not send again because the item is no longer approved. A response naming another destination is held for investigation. A matching destination and message ID produce a receipt. These tests exercise decisions; they do not establish real API permissions or network reliability.
In a durable implementation, persist the attempt before making the request, claim it atomically, and hold attempts left in sending after restart for reconciliation. The sketch above does none of that persistence. Also bind approval to a content revision, check per-channel pause immediately before delivery, and limit any explicitly eligible retry.
Rejection and uncertainty are different states
The Telegram Bot API documents the returned Message for a successful send. Its ResponseParameters include retry_after for flood control. An explicit rate-limit rejection can be treated according to that response; a lost response needs reconciliation instead.
Inspect the destination before deciding whether to retry an uncertain attempt. Record the message URL if it already exists. Do not infer success from a local function returning, and do not infer failure merely from a missing receipt.
For the broader editorial workflow, I wrote An AI publishing workflow needs a delivery receipt. This DEV version concentrates on the testable Python decision boundary.
Top comments (0)