Getting your python telegram bot working on Render
Does "telegram.error.Conflict: Conflict: terminated by other getUpdates request"; make sure that only one bot instance is running
sound familiar? If you’re trying to host your python telegram bot on Render, you’ve probably seen this error.
Telegram bots get messages in one of two ways: through polling (done via getUpdates
) or through webhooks. If your bot script is using application.run_polling(...)
, then you’re using getUpdates
.
Render’s web service infrastructure is constantly probing your services using the health check to see if they’re healthy. If the health check times out, returns nothing, or returns a negative response, Render will deploy another instance to attempt to replace the unhealthy one. For web services, Render’s health checks only come in the form of an HTTP request, so you need something in your running service that can handle that reequest.
Most Telegram bots that use the run_polling
method are not attached to web servers, and can’t handle the request from Render’s health check. Without a mechanism to tell render that your service is healthy, even though your service is doing what it’s supposed to, Render will think it’s unhealthy and deploy a new instance of it. And that’s when the error begins: you now have 2 instances of your bot running, and the getUpdates
API doesn’t accept that and emits the Conflict
error.
How do we get around this?
Paid option: Background workers
If you’re open to paying, background workers are actually a better solution for this problem. Render’s background workers are designed to run long-running processes, and they don’t have health checks. So you can just drop in your bot on a background worker and not have to worry about the health check causing Render to create new instances.
Free: Switch to webhooks
We can still use Render to host our bot, but we have to switch to using webhooks to survive the health check. The python-telegram-bot example library has an example webhook implementation bot we can simplify and adapt to be an echobot, and just deploy with the python runtime so no Docker image is needed:
import asyncio
import logging
import os
import uvicorn
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import PlainTextResponse, Response
from starlette.routing import Route
from telegram import Update
from telegram.ext import (
Application,
ContextTypes,
filters,
MessageHandler,
)
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# Define configuration constants
URL = os.environ.get("RENDER_EXTERNAL_URL") # NOTE: We need to tell telegram where to reach our service
PORT = 8000
TOKEN = "<YOUR TOKEN>"" # nosec B105
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Echo the user message."""
await update.message.reply_text(update.message.text)
async def main() -> None:
"""Set up PTB application and a web application for handling the incoming requests."""
application = (
Application.builder().token(TOKEN).updater(None).build()
)
# register handlers
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
# Pass webhook settings to telegram
await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES)
# Set up webserver
async def telegram(request: Request) -> Response:
"""Handle incoming Telegram updates by putting them into the `update_queue`"""
await application.update_queue.put(
Update.de_json(data=await request.json(), bot=application.bot)
)
return Response()
async def health(_: Request) -> PlainTextResponse:
"""For the health endpoint, reply with a simple plain text message."""
return PlainTextResponse(content="The bot is still running fine :)")
starlette_app = Starlette(
routes=[
Route("/telegram", telegram, methods=["POST"]),
Route("/healthcheck", health, methods=["GET"]),
]
)
webserver = uvicorn.Server(
config=uvicorn.Config(
app=starlette_app,
port=PORT,
use_colors=False,
host="0.0.0.0", # NOTE: Render requires you to bind your webserver to 0.0.0.0
)
)
# Run application and webserver together
async with application:
await application.start()
await webserver.serve()
await application.stop()
if __name__ == "__main__":
asyncio.run(main())
Some things to note here:
-
URL = os.environ.get("RENDER_EXTERNAL_URL")
. Because we receive notifications via webhooks, we have to tell Render where to find our service. All web services are given public URLs that end with.onrender.com
by Render, and that URL is actually available to us as an environment variable. If you’re using your own domain, you can use that instead. -
host="0.0.0.0"
. Render needs us to bind our webserver to all ip addresses for a machine, so the loopback address (127.0.0.1) shouldn’t be used.
Remember the Render free tier will put your service to sleep after 15 minutes of inactivity, so without activity from Telegram, you’ll need to ping your service to keep it awake. If you want to prevent it from sleeping, you can use my free keepalive tool to keep it awake and responsive.