Feature: Support SQLite file sessions for persistent update state
Problem
When using TELEGRAM_SESSION_STRING (StringSession), Telethon has no persistent update state (pts/qts/date/seq). Every time the MCP server restarts, Telegram floods the client with all accumulated updates across all chats/channels since the last online time.
For accounts with many channel subscriptions, this means thousands of updates.getDifference requests on every reconnect — which:
- Blocks the asyncio event loop processing the flood
- Causes MCP clients (e.g. mcporter) to timeout waiting for responses
- Triggers process spawning cascades (client thinks server is dead → spawns new one → old one still processing flood → new one also gets flooded)
- Can lead to OOM kills on memory-constrained servers
PR #140 (v3.1.17) fixed the blocking issue by moving entity cache warming to background. But the root cause — StringSession not persisting update state — remains.
How Telegram update state works
Telegram tracks a cursor (pts, qts, date, seq) for each client. On reconnect:
- With persisted state: client sends
updates.getState → Telegram returns only missed delta (a few updates)
- Without persisted state (StringSession): Telegram treats it as a fresh client → dumps all accumulated updates
Official Telegram apps (phone, desktop) use local SQLite databases to persist this state.
Solution
Telethon already supports SQLite file sessions via TelegramClient(session_name, api_id, api_hash) where session_name is a string (not a StringSession object). This creates a .session SQLite file that stores:
- ✅ Auth key (same as StringSession)
- ✅ Update state (pts/qts/date/seq) — the missing piece
- ✅ Entity cache (access_hash for users/chats/channels)
- ✅ Sent files cache
Current code (runtime.py)
session_string = os.getenv("TELEGRAM_SESSION_STRING")
session_name = os.getenv("TELEGRAM_SESSION_NAME")
if session_pool:
accounts["default"] = _build_client(
StringSession(_acquire_session(session_pool)), "default"
)
elif session_string:
accounts["default"] = _build_client(StringSession(session_string), "default")
elif session_name:
accounts["default"] = _build_client(session_name, "default")
TELEGRAM_SESSION_NAME already creates a file-based session! But it is buried under TELEGRAM_SESSION_STRING in priority, and there is no migration path or documentation warning about the update flood issue.
Important gotcha: because runtime.py calls load_dotenv(), if both TELEGRAM_SESSION_STRING and TELEGRAM_SESSION_NAME are present in .env, StringSession wins. Users must explicitly comment out / remove TELEGRAM_SESSION_STRING for the file-based session to take effect.
Proposed changes
1. Migration script (migrate_session.py)
A script that converts an existing StringSession to a SQLite file session:
#!/usr/bin/env python3
"""Migrate StringSession to SQLite file session for persistent update state."""
import asyncio, os, sys
from dotenv import load_dotenv
from telethon import TelegramClient
from telethon.sessions import StringSession
load_dotenv()
API_ID = int(os.getenv("TELEGRAM_API_ID"))
API_HASH = os.getenv("TELEGRAM_API_HASH")
SESSION_STRING = os.getenv("TELEGRAM_SESSION_STRING")
TARGET = os.getenv("TELEGRAM_SESSION_FILE", "telegram_mcp_session")
async def migrate():
if not SESSION_STRING:
print("Error: TELEGRAM_SESSION_STRING not set", file=sys.stderr)
sys.exit(1)
old = TelegramClient(StringSession(SESSION_STRING), API_ID, API_HASH)
await old.connect()
if not await old.is_user_authorized():
print("Error: session not authorized", file=sys.stderr)
sys.exit(1)
dc_id = old.session.dc_id
server_address = old.session.server_address
port = old.session.port
auth_key = old.session.auth_key
await old.disconnect()
new = TelegramClient(TARGET, API_ID, API_HASH)
await new.connect()
new.session.set_dc(dc_id, server_address, port)
new.session.auth_key = auth_key
new.session.save()
if await new.is_user_authorized():
me = await new.get_me()
print(f"Migration successful: {me.first_name} (id={me.id})")
else:
print("Migration failed", file=sys.stderr)
sys.exit(1)
await new.disconnect()
print(f"File {TARGET}.session created.")
print(f"Set TELEGRAM_SESSION_NAME={TARGET} in .env")
print(f"Comment out TELEGRAM_SESSION_STRING in .env")
asyncio.run(migrate())
2. Documentation
Add a section to README recommending file-based sessions for long-running servers:
## Recommended: File-based session for servers
StringSession (`TELEGRAM_SESSION_STRING`) does not persist Telegram update state
(pts/qts/date/seq). Every server restart triggers a full update flood from Telegram
across all chats and channels — potentially thousands of requests.
For long-running servers, use a file-based session:
1. Migrate your existing session:
```bash
uv run migrate_session.py
- In
.env, comment out StringSession and add file-based session:
# TELEGRAM_SESSION_STRING=...
TELEGRAM_SESSION_NAME=telegram_mcp_session
The .session SQLite file stores update state locally. On reconnect, Telegram sends
only the missed delta instead of flooding.
### 3. Optional: Warning when using StringSession
Print a warning when `TELEGRAM_SESSION_STRING` is used without a file-based session:
```python
elif session_string:
print(
"Warning: TELEGRAM_SESSION_STRING (StringSession) does not persist "
"update state. For long-running servers, consider migrating to a "
"file-based session (TELEGRAM_SESSION_NAME). See: migrate_session.py",
file=sys.stderr
)
accounts["default"] = _build_client(StringSession(session_string), "default")
Verification
After migration, the SQLite session file contains:
Tables: version, sessions, entities, sent_files, update_state
update_state: (id=0, pts=150548, qts=0, date=1785190768, seq=76796)
entities cached: growing as dialogs are fetched
On restart, pts is preserved → Telegram returns only delta updates → no flood.
Tested with:
mcporter list → telegram online, 116 tools
- Process count stays at 2 (uv launcher + python), no spawn cascade
- Zero
Got difference for channel entries in logs after restart
send_message and get_messages work correctly
Feature: Support SQLite file sessions for persistent update state
Problem
When using
TELEGRAM_SESSION_STRING(StringSession), Telethon has no persistent update state (pts/qts/date/seq). Every time the MCP server restarts, Telegram floods the client with all accumulated updates across all chats/channels since the last online time.For accounts with many channel subscriptions, this means thousands of
updates.getDifferencerequests on every reconnect — which:PR #140 (v3.1.17) fixed the blocking issue by moving entity cache warming to background. But the root cause — StringSession not persisting update state — remains.
How Telegram update state works
Telegram tracks a cursor (
pts,qts,date,seq) for each client. On reconnect:updates.getState→ Telegram returns only missed delta (a few updates)Official Telegram apps (phone, desktop) use local SQLite databases to persist this state.
Solution
Telethon already supports SQLite file sessions via
TelegramClient(session_name, api_id, api_hash)wheresession_nameis a string (not a StringSession object). This creates a.sessionSQLite file that stores:Current code (
runtime.py)TELEGRAM_SESSION_NAMEalready creates a file-based session! But it is buried underTELEGRAM_SESSION_STRINGin priority, and there is no migration path or documentation warning about the update flood issue.Important gotcha: because
runtime.pycallsload_dotenv(), if bothTELEGRAM_SESSION_STRINGandTELEGRAM_SESSION_NAMEare present in.env, StringSession wins. Users must explicitly comment out / removeTELEGRAM_SESSION_STRINGfor the file-based session to take effect.Proposed changes
1. Migration script (
migrate_session.py)A script that converts an existing StringSession to a SQLite file session:
2. Documentation
Add a section to README recommending file-based sessions for long-running servers:
.env, comment out StringSession and add file-based session:The
.sessionSQLite file stores update state locally. On reconnect, Telegram sendsonly the missed delta instead of flooding.
Verification
After migration, the SQLite session file contains:
On restart,
ptsis preserved → Telegram returns only delta updates → no flood.Tested with:
mcporter list→ telegram online, 116 toolsGot difference for channelentries in logs after restartsend_messageandget_messageswork correctly