Storage Engines

Every time you login to Telegram, some personal piece of data are created and held by both parties (the client, wzgram and the server, Telegram). This session data is uniquely bound to your own account, indefinitely (until you logout or decide to manually terminate it) and is used to authorize a client to execute API calls on behalf of your identity.


Persisting Sessions

In order to make a client reconnect successfully between restarts, that is, without having to start a new authorization process from scratch each time, wzgram needs to store the generated session data somewhere.

Different Storage Engines

wzgram ships four kinds of storage engine. Two keep the session on the machine that is running — a File Storage and a Memory Storage, both backed by SQLite — and two keep it somewhere that outlives the machine: a Remote Storage (MongoDB or Redis) and a Hybrid Storage, which is a local cache in front of a remote one.

Start with File Storage. Reach for the others when the host is ephemeral, or when several deployments need to look at the same sessions.

File Storage

This is the most common storage engine. It is implemented by using SQLite, which will store the session details. The database will be saved to disk as a single portable file and is designed to efficiently store and retrieve data whenever they are needed.

To use this type of engine, simply pass any name of your choice to the name parameter of the Client constructor, as usual:

from pyrogram import Client

async with Client("my_account") as app:
    print(await app.get_me())

Once you successfully log in (either with a user or a bot identity), a session file will be created and saved to disk as my_account.session. Any subsequent client restart will make wzgram search for a file named that way and the session database will be automatically loaded.

Memory Storage

In case you don’t want to have any session file saved to disk, you can use an in-memory storage by passing True to the in_memory parameter of the Client constructor:

from pyrogram import Client

async with Client("my_account", in_memory=True) as app:
    print(await app.get_me())

This storage engine is still backed by SQLite, but the database exists purely in memory. This means that, once you stop a client, the entire database is discarded and the session details used for logging in again will be lost forever.

Session Strings

In case you want to use an in-memory storage, but also want to keep access to the session you created, call export_session_string() anytime before stopping the client…

from pyrogram import Client

async with Client("my_account", in_memory=True) as app:
    print(await app.export_session_string())

…and save the resulting string. You can use this string by passing it as Client argument the next time you want to login using the same session; the storage used will still be in-memory:

from pyrogram import Client

session_string = "...ZnUIFD8jsjXTb8g_vpxx48k1zkov9sapD-tzjz-S4WZv70M..."

async with Client("my_account", session_string=session_string) as app:
    print(await app.get_me())

Session strings are useful when you want to run authorized wzgram clients on platforms whose ephemeral filesystems make a file-based storage engine impractical.

wzgram’s strings start with WZ_ and carry a CRC32, so a string mangled in transit is told apart from one that is merely in an older format, and every format wzgram has ever exported still decodes. Session Strings covers the format, the repair pass and what a string is safe to be stored in.

Warning

A session string is a live login. Anyone holding it is you, with no second factor in the way. Keep it out of version control, and revoke it by terminating the session from a logged-in client rather than by deleting the string.

Remote Storage

A file on disk is the wrong place for a session when the host is replaced on every deploy. MongoStorage and RedisStorage keep it in a database instead, so a container that restarts resumes with its peers already warm.

Drivers are optional and imported only when the storage is opened:

$ pip install "wzgram[mongo]"      # motor
$ pip install "wzgram[redis]"      # redis
from pyrogram import Client
from pyrogram.storage import MongoStorage

app = Client(
    "my_account",
    api_id=api_id, api_hash=api_hash,
    storage_engine=MongoStorage("my_account", "mongodb://localhost:27017"),
)

app.run()

Both engines take either a connection URI, which they own and close, or an already-created client to share with the rest of your program:

from motor.motor_asyncio import AsyncIOMotorClient
from pyrogram.storage import MongoStorage, RedisStorage

MongoStorage("my_account", AsyncIOMotorClient(uri), database="sessions")
RedisStorage("my_account", redis_client, prefix="bots:my_account")

Warning

In Redis, an evicted session key is a lost login. Peers are a cache and can be evicted safely, but the session hash cannot — run the database with maxmemory-policy noeviction, or give wzgram a database of its own. Opening the storage logs a warning when the server reports any other policy.

Coming from pyrofork’s MongoStorage? Its documents carry no server_address or port. MongoStorage.import_pyrofork() reads that layout once and writes wzgram’s, resolving the address from the datacenter id.

Hybrid Storage

A remote database is durable, but it is also a network round trip, and resolve_peer runs on every single send. HybridStorage puts a local SQLite cache in front of any backend:

from pyrogram import Client
from pyrogram.storage import HybridStorage, MongoStorage

app = Client(
    "my_account",
    storage_engine=HybridStorage(
        "my_account",
        backend=MongoStorage("my_account", "mongodb://localhost:27017"),
    ),
)

app.run()

What it does:

  • Reads are local. On open() the backend is copied into the cache; after that every peer lookup and every session attribute is answered without touching the network.

  • Writes are mirrored in the background. They land in the cache immediately and reach the backend through a bounded queue drained by a background task, coalesced by key.

  • A remote outage is not a client outage. A failed write is retried with backoff and logged; reads keep working and the client keeps running.

  • Under pressure, the right writes survive. When the queue is full, peer writes are dropped oldest-first — losing one costs a single lookup. Session attributes and update state are never dropped: losing those costs the login, or restarts gap recovery from a stale pts.

The trade-off is honest and worth stating: the last writes live only in the cache until the writer drains. close() flushes them, bounded by flush_timeout; a SIGKILL does not.

Pass cache_in_memory=False to keep the cache in a file, which survives a restart and skips the bulk load on open.

Writing your own engine

Subclass RemoteStorage rather than Storage: it implements the whole contract on top of a handful of primitives, and it brings the two caches that keep the hot path off the network.

Primitive

What it does

_connect() / _disconnect()

open and close the driver

_load_session()

the session as a dict, or None if there is none yet

_save_session(fields)

upsert those fields

_upsert_peers(rows)

(id, access_hash, type, phone_number) tuples

_fetch_peer(peer_id)

(id, access_hash, type, last_update_on) or None

_fetch_peer_by_username(username)

same shape

_fetch_peer_by_phone(number)

same shape

_replace_usernames(rows)

delete-then-insert per peer id

_load_states() / _save_state(t) / _delete_state(id)

update state

_purge(remove_peers)

what delete() does

Three rules the base already follows, and a hand-written engine has to keep:

  • Session attributes are read once, then served from memory. dc_id() runs on every send; it must not be a round trip.

  • ``update_peers`` skips peers whose access hash has not changed. Every invoke feeds r.users and r.chats back through fetch_peers, so without that filter the same unchanged peers are rewritten on every RPC.

  • The username TTL is enforced on read, not by an expiry feature of the store. A backend that drops the row itself would disagree with what SQLite does with a stale one.

Version your layout with a VERSION constant and implement _load_version / _save_version / _migrate, so a change of shape can migrate rather than corrupt.

Reusing a Telethon session

To reuse a Telethon session (the two formats are not compatible on their own), there is a community storage engine that reads and writes Telethon’s layout, so the same session file stays usable from both libraries.

from pyrogram import Client
from .tele_storage import TelethonStorage  # assumes that the path downloaded is accurate

workdir = Path(__file__).parent
test_mode = False
is_bot = False # Pass True if your session is bot session

async with Client(
    "my_account",
    api_id=api_id,
    api_hash=api_hash,
    lang_code="ru",
    workdir=workdir,
    test_mode=test_mode,
    storage_engine=TelethonStorage(
        name="my_account",
        workdir=workdir,
        api_id=api_id,
        test_mode=test_mode,
        is_bot=is_bot
    )
) as app:
    await app.send_message(chat_id="me", text="Greetings from **wzgram**!")