Handbook: Implementing Non-Blocking Telegram Notifications in a Synchronous Flask/Gunicorn Application
1. The Problem: App Crashes and Unexpected token '<'
Errors
When attempting to add a feature like sending a Telegram notification after a web request, a common mistake is to introduce code that conflicts with the application’s execution model. In our case, we encountered an Unexpected token '<', "<!DOCTYPE "... is not valid JSON
error on the frontend.
Diagnosis: This error indicates that the frontend expected a JSON response from the API but received an HTML error page instead. This happens when the backend Flask application crashes mid-request due to an unhandled exception.
The root cause of the crash was attempting to run asynchronous code (from the python-telegram-bot
library using asyncio
) inside a synchronous Flask application managed by Gunicorn’s synchronous workers. This creates a conflict between event loops and process models, leading to instability and crashes.
2. The Wrong Solutions & Why They Failed
- Directly using
asyncio.run()
: Callingasyncio.run()
within a synchronous Flask route handler blocks the entire Gunicorn worker process. If the network call to Telegram is slow, the worker cannot handle any other requests and may be terminated by Gunicorn for timing out, causing the application to fail. - Mixing
async
libraries with a sync framework: Relying on an async-first library (python-telegram-bot
) in a sync-first framework (Flask) requires complex configurations (like using an ASGI server like Uvicorn instead of Gunicorn’s sync workers), which adds unnecessary complexity for a simple notification task.
3. The Correct, Robust Solution: Threading and a Synchronous HTTP Client
The goal is to send the notification without blocking the main application thread from returning a response to the user. The most reliable way to achieve this in a standard Flask/Gunicorn setup is by offloading the task to a separate background thread.
The principles are:
- Keep the Main App Synchronous: The core Flask application should remain simple and synchronous.
- Use a Synchronous Library for the Task: Instead of a complex async library, use the standard
requests
library to make a simple, synchronous HTTP call to the Telegram Bot API. - Offload to a Background Thread: Use Python’s built-in
threading
module to run the notification function in the background. This immediately frees up the main thread to finish the request.
4. Implementation Steps
Step 1: Use a Simple, Synchronous Function for Notifications
Create a standard Python function that uses requests
to call the Telegram API. This function is self-contained and easy to debug.
|
|
Step 2: Use a Decorator to Wrap the Flask Route Handler
A decorator is an elegant way to add logging and the notification logic without modifying the core business logic of the route handler itself.
|
|
Step 3: Apply the Decorator to Your Route
|
|
5. Key Takeaways to Avoid Future Errors
- Respect the Execution Model: Do not mix
asyncio
code in a standard synchronous Gunicorn/Flask worker. It leads to unpredictable crashes. - Offload, Don’t Block: For any time-consuming task that is not essential to the user’s immediate response (like sending an email, a webhook, or a Telegram message), use
threading.Thread
to run it in the background. - Keep It Simple: When a simple, synchronous tool like
requests
can do the job, prefer it over a complex asynchronous library that requires changing your entire application server model. - Decorators are Your Friend: Use decorators to cleanly separate cross-cutting concerns like logging and notifications from your core business logic.