📌 Expert Workflows · Step 3: AI Telegram Bot · Edition A (Office Worker) · Part 3 of 3 — final
⚙️ How to read this tutorial. This is a hands-on, error-first guide: it deliberately shows a common mistake first, explains why it happens, then gives the corrected, verified fix. Any failing command or error message below is an intentional teaching example, clearly marked BEFORE; the working solution always follows, marked AFTER. The post ends on a tested, successful result.
🎯 What You’re Actually Building Here
Part 2 defines summarizing and drafting commands. This final part adds sanitized error reporting, a local rolling request limit and process uptime. Model accuracy and refusal reliability remain unmeasured.
The operational failures to test are missing input, provider throttling and a stopped local process. Their symptoms can overlap, so diagnose the observed error instead of assuming a cause.
The examples below cover those failure paths in one sequential process. Hosting, concurrent users and restart recovery require additional design and testing.
Project context
Edition A — Office Worker. This is an instructional operations-team scenario, not a record of a real employee, six-person deployment or measured time saving.
See the published curriculum for available projects.
🛠️ What to Have Ready
- Parts 1 and 2 configured; use non-sensitive test inputs and review generated answers
- The Part 2 project files and a terminal; setup time depends on your environment
📝 Step 1: One Crash Shouldn’t Kill Everything
An unexpected handler error can leave a request without a useful reply. python-telegram-bot routes handler errors through its error handling; one exception does not necessarily terminate the process.
Save the following helper module as operations.py beside bot.py. The next snippet imports and registers it:
"""Single-process, sequential-update teaching helpers. No live dependencies."""
import logging
import math
import time
from collections import defaultdict
logger = logging.getLogger(__name__)
user_calls = defaultdict(list)
COOLDOWN_SECONDS = 10
DAILY_LIMIT_PER_USER = 25
STARTED_AT = time.monotonic()
def check_rate_limit(user_id, now=None):
now = time.monotonic() if now is None else now
calls = user_calls[user_id]
calls[:] = [t for t in calls if now - t < 86400]
if calls and now - calls[-1] < COOLDOWN_SECONDS:
return f"Wait {math.ceil(COOLDOWN_SECONDS - (now - calls[-1]))} seconds."
if len(calls) >= DAILY_LIMIT_PER_USER:
wait = math.ceil(86400 - (now - calls[0]))
return f"Rolling 24-hour limit reached. Try again in {wait} seconds."
calls.append(now)
return None
async def error_handler(update, context):
logger.error("Handler failure: %s", type(context.error).__name__)
message = getattr(update, 'effective_message', None)
if message is not None:
try:
await message.reply_text("The request failed. Contact the bot owner if it persists.")
except Exception:
logger.error("Failure notification could not be delivered.")
async def status_cmd(update, context):
message = getattr(update, 'effective_message', None)
if message is not None:
minutes = int((time.monotonic() - STARTED_AT) // 60)
await message.reply_text(f"This process has been running for {minutes} minutes. Provider health is not checked.")
Register it in main():
# At the top of bot.py:
from operations import check_rate_limit, error_handler, status_cmd
# Inside main(), before app.run_polling():
app.add_error_handler(error_handler)
The error handler records only the exception class and attempts a generic reply. It does not guarantee process survival or message delivery; its own delivery failure is caught and logged without exposing the original content.
The reply directs the user to the bot owner without showing credentials, provider payloads or private messages. Diagnose failures locally using minimal information.
Evidence boundary. The scenarios illustrate failure modes. The recorded results below come from actual offline executions with synthetic inputs and substituted clients, not a workplace deployment.
Troubleshooting 5 — Missing text or captions
(Teaching scenario — a common mistake reconstructed for learning, not a personal claim.)
🔴 BEFORE
An unguarded access such as update.message.text fails when update.message is absent. An illustrative exception is:
AttributeError: 'NoneType' object has no attribute 'text'
🤔 Cause
A message object can exist while its text is None, and some update types have no update.message at all. The displayed error corresponds to accessing .text on a missing object. A photo caption is a separate field; calling string methods on missing text is another failure.
There’s a broader version of this worth internalising.Users send you things you didn’t imagine.Not maliciously; they just use the tool the way it looks like it should work. Every input assumption you make is a bug waiting for the colleague who does something slightly different.
🟢 AFTER
Defend at the point of extraction, and say something useful rather than crashing:
def extract_target_text(update, context):
message = update.effective_message
if message is None:
return ""
reply = message.reply_to_message
if reply is not None:
return (reply.text or reply.caption or "").strip()
return " ".join(context.args or []).strip()
And in the handler, be explicit aboutwhynothing happened:
# In run_ai_command, after the effective_message guard:
if not text:
await message.reply_text("Provide text or reply to a text/caption message.")
return
Noteupdate.effective_messagerather thanupdate.message. Edited messages, channel posts and some other update types leaveupdate.messageempty, and that’s another silent crash you’ve now prevented.
Test takeaway: include missing-message updates, captioned photos and stickers in the input cases. The complete code already rejects unsupported empty input before an AI request.
📝 Step 2: Rate Limits — Protecting the Quota From Enthusiasm
Providers impose account- and model-specific request, token or spending limits. The local counter below cannot infer those limits or the remaining account balance.
This teaching limiter tracks one sequential process in memory. It is not a global spending cap, persistent database or concurrency-safe distributed limiter.
# Already defined in operations.py above:
# COOLDOWN_SECONDS = 10
# DAILY_LIMIT_PER_USER = 25
# check_rate_limit(user_id) returns None or a retry message.
Then guard each AI command:
# Inside run_ai_command, after empty/overlong input checks
# and before the try block that calls the provider:
if update.effective_user is None:
await message.reply_text("A user identity is required for this command.")
return
blocked = check_rate_limit(update.effective_user.id)
if blocked:
await message.reply_text(blocked)
return
The policy permits at most 25 accepted attempts per user in a rolling 24-hour window, with a 10-second cooldown. Failed provider requests still consume an accepted attempt. Restarting clears the counters. Multiple users, token-based costs and provider retries can still exceed an account budget.
Troubleshooting 6 — Provider throttling or quota errors
(Teaching scenario — a common mistake reconstructed for learning, not a personal claim.)
🔴 BEFORE
A provider may return a throttling or quota error. This illustrative message is a symptom, not evidence of how many requests caused it:
Sorry — that didn't work: Error code: 429
A 429 alone does not establish that the account is blocked until tomorrow. Inspect the provider’s documented error and retry guidance without posting its raw payload in the chat.
🤔 Cause
A request count is not a cost calculation. A short and long input may each make one request, but token-based charges and rate limits can differ.
Set provider-side spending controls where available and explain the local limit to testers. Do not substitute the per-user counter for account-level monitoring.
🟢 AFTER
The complete version uses the rolling limiter above. It does not include a minimum-length rule or a remaining-count display; empty and overlong input are handled by Part 2.
When blocked, the limiter returns a wait interval. It uses monotonic time for elapsed intervals, not a calendar-midnight reset.
Observed local result: the 26th attempt inside a rolling window was blocked and an expired attempt stopped counting after 24 hours. These synthetic tests do not measure user behavior or usage reduction.
📝 Step 3: The Laptop Problem
Here’s the one you can’t code around, and it’s worth stating plainly.
Polling requires a running process and network connection. Closing its terminal, sleeping the machine or restarting interrupts availability. Tell test participants the actual availability boundary.
Three interim options, honestly assessed:
Keep the laptop awake.Works, costs you a machine you can’t close, and dies the moment you reboot. Fine for a week-long trial, not a foundation.
Use an approved spare machine. It still needs updates, network access, secure credential handling and monitoring; it does not establish reliable hosting by itself.
Set expectations. The /status response reports this process’s uptime. It does not check provider health, billing or end-to-end availability; no response does not identify the cause.
# Inside main(), before app.run_polling():
app.add_handler(CommandHandler("status", status_cmd))
An approved host can remove dependence on your laptop, but verify its current pricing, sleep behavior, persistent storage, secrets handling and monitoring. This article does not establish a free or always-on deployment.
⚠️ Don’t Close the Tab Yet
You now own something other people depend on.That’s a real obligation, and it’s worth deciding deliberately rather than drifting into it. What happens when you’re on leave? Does anyone else have the token? If you left the company tomorrow, does the bot die quietly with your laptop, along with whatever your team stopped doing manually because it existed?
You don’t need a formal answer. You do need one sentence of honesty to your team about what this is: a useful personal project that helps, not a supported service. Colleagues calibrate perfectly well when told; they calibrate badly when left to assume.
Before involving other people, explain the permitted inputs, provider destination, availability and contact for failures. Agree who owns the test and how it will be stopped.
Minimize logs. The included error helper records exception classes, not message content. Add aggregate usage and outcome counts only when needed; user identifiers are also data and require a defined purpose and retention period.
Revisit data handling. Only text accepted by the AI command path is sent to the configured model provider. Empty, overlong and locally blocked requests are rejected first. Review the exact account’s terms before workplace use.
✅ What You’ve Built
The complete project handles the tested missing-input cases, splits long output, applies a per-user rolling limit, reports process uptime and sanitizes handler errors. Updates remain sequential by default. Live delivery, concurrency, model accuracy and dependable refusal are outside the offline results.
The three-part prototype is complete within that boundary. Uptime, persistent limits and provider behavior still need environment-specific testing before wider use.
💡 For the Ambitious
An optional admin-only usage command should first reject missing user identity, then compare the sender ID with an explicitly configured owner ID. Define its window and retention before implementing it; no such command is included here.
❓ Put It Into Practice
In a private test chat with informed participants, try missing text, captions, cooldown and process shutdown. Record observed replies and recovery without logging private content.
Local experiment and downloadable result
Download the complete Part 2/3 project and recorded tests. For this lesson, copy bot_part3.py to bot.py and keep operations.py beside it. Use the virtual environment and provider configuration from Part 2. The download already contains all imports, command registrations and the limiter call, so the short integration snippets above need not be pasted again.
Run python verify.py with the archive filenames unchanged. The 7 September 2026 experiment used Python 3.11.4 and synthetic inputs; all 22 checks passed again on 10 September. The archive includes the executed test and test-results.json.
| Executed local case | Observed result |
|---|---|
| Missing message, sticker, caption and command arguments | Handled without an unguarded text lookup; captions and arguments extracted |
| Empty and 8,001-character input | No AI call |
| 4,500-character Unicode reply | Three chunks rejoined to the original string |
| First request, cooldown and independent users | Expected allow/block decisions |
| 26th rolling-window request and 24-hour expiry | Blocked at the cap; permitted after expiry |
| Provider failure and unexpected handler error | Generic user message; diagnostic excludes injected content |
| Status and final application registrations | Uptime boundary present; status/error handlers registered |
Test boundary: Telegram and AI clients were substituted. This was an actual offline code experiment, not a live deployment or a model-quality test. It does not measure uptime, costs, real-user behaviour or concurrency. The limiter is intentionally limited to one sequential process and loses state on restart.
🔗 What’s Next
Step 3 is complete for this edition. The Expert Workflows roadmap lists the current published path.
Choose and test hosting only after reviewing the service’s current limits and your intended data use. This series does not promise a free production deployment.
✍️ About the author & how this was made
Written and edited byG. Troy, editor of Life Tech Hack. The site is an independent AI-education blog; its approach draws on hands-on work across engineering, design, and several other industries, and every article is checked against primary sources (official docs, specs, and vendor guidance) before publishing. Where a claim depends on a price, feature, or policy that can change, the relevant source is linked inline.
AI tools may be used to help draft or organise material; the final content is human-reviewed and edited, and unsupported claims or invented quotations are removed. This reflects the site’s editorial standards and Google’s guidance on helpful, people-first content.

![[Step 3-A-3/3] Keeping It Running — Errors, Limits, and the Laptop Problem (Office Worker Edition)](https://lifetechhack.com/wp-content/uploads/2026/09/expert-948-errors.png)