· 6 min read
Reducing Bot Latency by Reusing Telegram API Connections
A common source of avoidable latency in Telegram bots is repeated connection setup for every API call. This guide shows how to reduce round-trip time, smooth burst traffic, and improve throughput by reusing HTTP connections, tuning pools, and measuring the real bottlenecks.
If your Telegram bot feels slow under load, the problem is often not your business logic. It is usually the cost of repeatedly creating outbound connections to the Telegram Bot API: DNS lookup, TCP handshake, TLS negotiation, and connection teardown. These costs are small per request, but they add up quickly when your bot sends many messages, edits, or callback responses.
The most effective performance improvement for many Telegram integrations is simple:
reuse HTTP connections instead of opening a fresh one for every API request.
This guide explains why it matters, how to implement it safely, and how to validate that it actually improves latency in your bot.
Why connection reuse matters
A Telegram bot typically performs lots of short-lived HTTPS requests:
sendMessageanswerCallbackQueryeditMessageTextsendPhotodeleteMessage
If each request creates a new TCP/TLS session, you pay for:
- DNS resolution
- TCP 3-way handshake
- TLS handshake
- Server-side connection setup
- Kernel socket teardown on close
For a single request, the overhead may be acceptable. For bursty traffic, it becomes significant. Imagine 200 callback queries arriving in a few seconds. If each one triggers a new connection, your bot wastes CPU and network time before it even reaches Telegram’s application layer.
Connection reuse helps because:
- TLS session establishment is amortized across many requests
- latency becomes more stable under burst load
- fewer sockets are created and destroyed
- the process uses less CPU and fewer ephemeral ports
- throughput improves without changing bot logic
What to optimize first
Before touching code, confirm that request setup is the bottleneck.
Measure these separately:
- time spent in your handler before the API call
- time spent waiting for the Telegram API response
- queueing delay if you serialize requests
- socket creation time if your client exposes it
If you only look at end-to-end latency, you may optimize the wrong layer.
Useful metrics:
- median and p95 API latency
- requests per second per process
- connection pool reuse rate
- open sockets over time
- error rate during traffic bursts
The recommended approach
Use one long-lived HTTP client per process, or one per service instance, and configure it with connection pooling and keep-alive enabled.
Good properties of the client
Your Telegram API client should:
- keep connections alive
- reuse TLS sessions
- use a bounded connection pool
- apply reasonable idle timeouts
- avoid creating a new client per request
Bad pattern
This is the classic mistake:
# Anti-pattern: creating a new client per request
async def handle_update(update):
async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.telegram.org/bot<TOKEN>/sendMessage",
json={"chat_id": update.chat_id, "text": "Hello"},
) as resp:
return await resp.json()
This looks clean, but it creates and tears down a session for every call. In practice, that defeats connection reuse.
Better pattern: shared client session
Create the session once during application startup and reuse it for all API calls.
import aiohttp
class TelegramClient:
def __init__(self, token: str):
self.base_url = f"https://api.telegram.org/bot{token}"
self.session = None
async def start(self):
connector = aiohttp.TCPConnector(
limit=100,
ttl_dns_cache=300,
keepalive_timeout=30,
enable_cleanup_closed=True,
)
timeout = aiohttp.ClientTimeout(total=10, connect=3)
self.session = aiohttp.ClientSession(connector=connector, timeout=timeout)
async def close(self):
if self.session:
await self.session.close()
async def send_message(self, chat_id: int, text: str):
url = f"{self.base_url}/sendMessage"
payload = {"chat_id": chat_id, "text": text}
async with self.session.post(url, json=payload) as resp:
resp.raise_for_status()
return await resp.json()
Why this is better
- The connector maintains reusable sockets.
- DNS entries are cached for a period instead of looked up repeatedly.
- The pool size prevents unbounded concurrency.
- Startup and shutdown are explicit, which is easier to manage in production.
Pool sizing: avoid extremes
A common mistake is assuming bigger pools are always better. They are not.
Too small
If your pool is too small, requests queue behind available connections. That increases latency during bursts.
Too large
If your pool is too large, you can create unnecessary socket pressure and make the process harder to tune.
Practical guidance
Start with:
limitnear your expected concurrent outbound Telegram requests- a modest
keepalive_timeoutsuch as 15–60 seconds - a DNS cache TTL that matches your deployment pattern
Then observe real traffic.
If your bot mostly handles callback queries and message edits, concurrency may be low. If you fan out notifications to many chats, you may need a larger pool.
Apply backpressure instead of flooding the API
Connection reuse helps, but it does not fix overload by itself. If you enqueue too many outgoing requests, you can still create latency spikes.
Use a bounded worker model:
- batch non-urgent work
- limit concurrent sends
- coalesce repeated edits to the same message
- drop obsolete updates when newer state supersedes them
For example, if a user rapidly clicks inline buttons, there is no reason to send five intermediate edits if only the latest state matters.
A simple strategy is to deduplicate by message ID:
pending_edits = {}
async def schedule_edit(message_id, new_text):
pending_edits[message_id] = new_text
Then flush only the latest update per message on a timer or worker tick.
Optimize request patterns, not just transport
Reusing connections improves the transport layer, but many bots still waste time at the API design level.
Prefer edits over deletes plus sends
If a message can be updated in place, editMessageText is often cheaper than deleting and sending a new one. It reduces API calls and preserves chat history.
Avoid unnecessary round trips
If your handler only needs to acknowledge a callback, do not fetch extra state from Telegram unless required.
Cache data you already know
Examples:
- store
chat_idandmessage_idfrom the update instead of re-querying - cache user settings locally if they change infrequently
- avoid repeated
getChatcalls in hot paths
Every avoided request is a larger win than a faster request.
Handling long polling and webhooks
Transport reuse matters for both update delivery models, but the priorities differ.
Long polling
With long polling, your inbound connection is already long-lived. Outbound API calls are still where connection reuse pays off.
Webhooks
Webhook handlers must respond quickly. If you call Telegram API synchronously inside the request path, outbound latency directly increases webhook response time.
For webhooks, consider:
- acknowledging the webhook quickly
- dispatching heavier work to a background queue
- using a shared Telegram client in the worker process
This prevents your HTTP server from being tied up by slow outbound calls.
Common implementation mistakes
1. Creating a client inside each handler
This is the most expensive mistake.
2. Forgetting to close the shared session
If you reuse connections, you must still close the session during shutdown.
3. Using blocking HTTP libraries in async code
A blocking library inside an async bot can stall the event loop and make connection reuse irrelevant.
4. Ignoring retries
A reused connection does not eliminate transient network failures. Add retries carefully for idempotent operations, but avoid blindly retrying actions that may duplicate effects.
5. Measuring only average latency
Averages hide queueing and pool contention. Track p95 and p99.
How to verify the improvement
Do not ship this change on intuition alone. Benchmark it.
Benchmark setup
Compare two versions of your bot client:
- baseline: new client per request
- optimized: shared client with pooling
Test under:
- idle traffic
- moderate steady traffic
- burst traffic
What to record
- request latency distribution
- CPU usage
- memory usage
- open socket count
- outbound error rate
- throughput per process
Expected results
You should see:
- lower median latency
- much better p95 under burst load
- lower CPU spent on network setup
- fewer transient failures caused by resource churn
If the improvement is small, you may already have decent reuse or your bottleneck may be application logic, not transport.
Production checklist
Before rolling out, confirm:
- one shared HTTP client per process
- keep-alive enabled
- bounded pool size
- sensible DNS caching
- explicit startup and shutdown hooks
- metrics for latency and pool saturation
- background processing for non-urgent work
Example rollout strategy
If you maintain a production bot with traffic variability, roll this out gradually:
- Add timing logs around Telegram API calls.
- Replace per-request client creation with a shared client.
- Set conservative pool limits.
- Observe p95 latency and error rates.
- Increase limits only if you see connection contention.
- Add request deduplication for bursty paths.
This sequence keeps the optimization measurable and reversible.
Conclusion
For Telegram developers, one of the highest-value performance improvements is to reuse outbound API connections instead of recreating them for each request. This reduces handshake overhead, improves burst handling, and lowers CPU cost without changing bot behavior.
If your bot already uses a shared client, the next gains usually come from better request shaping: fewer redundant API calls, tighter concurrency limits, and more careful use of edits, caching, and background workers. Start by measuring connection churn, then fix the transport layer before chasing deeper micro-optimizations.