Skip to content

Upgrading

Most upgrades are a pin change and a restart. A few releases need an extra step, listed below.

The routine upgrade

  1. Read the changelog for every release between yours and the new one. Check the table of releases that need action below.
  2. Take a backup of the archive. See Backing up the archive.
  3. Move the pin of both images to the same new version. If you run from a clone of the repository, git pull brings the new pins in the stock docker-compose.yml. Otherwise edit both image: lines yourself:

    services:
      telegram-backup:
        image: drumsergio/telegram-archive:9.8.0
      telegram-viewer:
        image: drumsergio/telegram-archive-viewer:9.8.0
    
  4. Pull and recreate the containers:

    docker compose pull && docker compose up -d
    
  5. Reload any viewer tab that was open during the upgrade.

docker compose pull on its own only pulls the tag you already pinned. Moving the pin is the upgrade.

The backup container migrates the database each time it starts. The viewer never migrates, so it returns errors for a few seconds while the backup migrates. SQLite and PostgreSQL covers the exceptions for the login command. Check the schema revision after the start:

docker compose exec telegram-backup alembic current

Migrations only go forward. You cannot downgrade to an older release. To go back, restore the backup you took in step 2 and pin the old version again.

Image tags

Tag What it is
9.8.0, v9.8.0 A release. The two names point at the same image. The backup and viewer images publish release tags for linux/amd64 and linux/arm64.
latest Rebuilt from pushes to main that touch the image's code. It can carry code that is not released yet. Do not use it.
dev Built from pull requests opened from the repository itself, linux/amd64 only. It is a test image.

Upgrading a pip install

A pip install does not migrate on its own. Upgrade the package, then run the migrations before you start the backup, the scheduler or the viewer:

pip install -U telegram-archive
telegram-archive --data-dir ./data migrate

Use the same --data-dir or database variables you run the archive with.

Run migrate before any other command on a new database. If another command created the database first, migrate fails on the first table it tries to create, with table chats already exists on SQLite or relation "chats" already exists on PostgreSQL. To fix it, delete the new database and run migrate first. You can also start the Docker backup image once. Its entrypoint detects this schema and marks it as current.

Packages on PyPI start with 8.17.0. See Install from PyPI.

The module rename

In 8.17.0 the code moved from the src package to telegram_archive.

  • Both images keep a src alias. python -m src ... and uvicorn src.web.main:app keep working and run the same code. They print a one-line deprecation note on stderr.
  • Images up to and including 8.16.1 only know src. On those, use python -m src.
  • The PyPI package has no src. Use telegram-archive or python -m telegram_archive.
  • The wallpaper mount path is now /app/telegram_archive/web/static/<file>. The old path, /app/src/web/static/<file>, still works.
  • From a checkout of the repository, run Alembic with alembic -c telegram_archive/alembic.ini. Inside the backup container a bare alembic still works.

The stock docker-compose.yml, init_auth.sh and init_auth.bat use the new name.

Releases that need action

Every release needs the routine upgrade. This table lists the extra steps, newest release first.

Release What to do
9.8.0 Nothing to change. No migration. The logo, the favicon and the installed-app icons are new; a browser that installed the viewer as an app keeps the old icon until it reads the manifest again.
9.7.0 Nothing to change. No migration. A JSON chat export that sends media, max_file_mb or players now gets a 400; drop those parameters, or add format=html.
9.6.0 Nothing to change. No migration. SYNC_DELETIONS_EDITS_DAYS is new and defaults to 0, a full pass as before.
9.5.1 Nothing to change. No migration.
9.5.0 Nothing to change. Migration 042 runs on the first start and adds six timestamp indexes (two of them on messages), so on a large archive that start takes longer than usual; let it finish.
9.4.2 Nothing.
9.4.1 Nothing, unless you sign in through OpenID Connect: register the viewer's root URL as a post-logout redirect URI at the provider, and note that changing VIEWER_USERNAME or removing an OIDC_ADMIN_USERS entry now ends that name's master sessions.
9.4.0 Nothing. OpenID Connect sign-in is off until you set the OIDC_* variables, and the QR login is a new auth --qr flag.
9.3.7 Nothing. Only a container that was started with no command changes: it now runs the archiver.
9.3.6 Nothing.
9.3.5 Nothing.
9.3.4 Nothing.
9.3.3 Nothing. Migration 041 runs on start and adds the media_transcripts columns a :dev image from 2026-09-26 created without; a table that has them is left alone.
9.3.2 Nothing.
9.3.1 Nothing.
9.3.0 Nothing required. With TRANSCRIPTION_URL set, schedule now sends waiting files to the transcription server every 15 minutes, not only after a full pass. See Transcription drains every 15 minutes. Photos, videos and GIFs in the viewer change size. To give older photos their real shape before they load, run telegram-archive check-media and then, if it reports Photos without size, check-media --repair; the repair also makes every other check-media repair. See Picture sizes. Migration 040 runs on start and adds a table for custom emoji; their files arrive with the next backup run. A script that reads a custom emoji entity's document_id from the viewer's API now gets a string. See Custom emoji and API clients.
9.2.1 Nothing.
9.2.0 An install that sets neither ENABLE_LISTENER nor SCHEDULE now runs the real-time listener and one full pass a day. Set ENABLE_LISTENER=false to keep the old behaviour. A .env or compose file copied from an older release keeps its values. See Upgrading to 9.2.0. Run telegram-archive check-media once. A dry run now exits 1 when it finds a video or audio file whose download stopped early, and --repair marks such files to download again. See the Files cut short section. Migration 039 runs on start and files old video stickers and animated stickers as stickers. If you set DOWNLOAD_MEDIA_TYPES, sticker now covers video stickers. See the Stickers section.
9.1.0 Nothing. To use an MTProxy, set TELEGRAM_PROXY_TYPE=mtproxy and TELEGRAM_PROXY_SECRET. See Proxy.
9.0.0 Scripts that read a chat export must change, and so must readers of raw_data.poll, raw_data.webpage and raw_data.entities, /ws/updates clients, and scripts that press the transcript button many times as a viewer login. The first start runs migrations 034 to 038; on a large archive it takes longer. After it, run telegram-archive check-media once, and telegram-archive backfill-details --apply once with the backup stopped. See Upgrading to 9.0.
8.18.0 Nothing. A browser that never picked a theme opens in Match system instead of Slate, unless VIEWER_DEFAULT_THEME pins a theme. A saved choice keeps working. See Themes and wallpaper.
8.17.0 Migration 033 runs on start. The module is now telegram_archive, and python -m src keeps working. See Upgrading to 8.17.0.
8.16.1 Nothing.
8.16.0 Migration 032 runs on start. Voice transcription is on but stays idle until TRANSCRIPTION_URL is set. Set TRANSCRIPTION_ENABLED=false to hide its banner. See Voice transcription.
8.15.1 Nothing.
8.15.0 DELETION_MODE now defaults to soft, and excluded chats are kept unless EXCLUDE_DELETE_EXISTING=true. If you relied on deletion, set DELETION_MODE=hard or EXCLUDE_DELETE_EXISTING=true before you upgrade. Migration 031 runs on start. Upgrade both images together.
8.14.0 SKIP_MEDIA_DELETE_EXISTING now defaults to false. Set it to true to keep deleting the media of skipped chats. Migrations 029 and 030 run on start.
8.13.0 A restricted viewer account sees zero statistics until the next daily calculation or a refresh by the master login. If you set DOWNLOAD_MEDIA_TYPES and want video_note to match, and the archive was captured before 8.5.0, run reclassify-round-videos once, with the backup stopped and the viewer idle. See Import and maintenance tasks.
8.12.1, 8.12.0, 8.11.3, 8.11.2, 8.11.1, 8.11.0 Nothing.
8.10.1 Reload any viewer tab left open across the upgrade.
8.10.0 DOWNLOAD_YOUTUBE_VIDEOS defaults to false, so the video file behind a YouTube link is no longer downloaded. Set it to true to keep the old behaviour. See Media downloads.
8.9.1 If the viewer mounts the archive read-only, set THUMBNAIL_CACHE_DIR to a writable volume. Without it the thumbnail cache is lost each time the container is recreated. See Thumbnails.
8.5.0 Round video messages captured before this release stay typed as ordinary videos. Run reclassify-round-videos once to correct them, with the backup stopped and the viewer idle. See Import and maintenance tasks.
8.3.0 Migration 028 runs on start and indexes every existing message for full-text search.
Other releases from 8.0.1 to 8.9.2 Nothing. Their migrations run on start.

Upgrading to 9.3.0

Transcription drains every 15 minutes

With TRANSCRIPTION_URL set, the schedule command now runs a transcription drain every 15 minutes, besides the one after each full pass. A pressed file, a backlog of TRANSCRIPTION_TYPES and akou results for files the listener sent no longer wait for the daily pass.

  • A backlog now drains at up to TRANSCRIPTION_BACKFILL_PER_RUN files per account every 15 minutes. With the default of 50 that is up to 4800 files a day per account, where the daily pass sent 50. There are 96 drains a day where there was one. On a paid provider (the OpenAI endpoint, Deepgram, AssemblyAI, ElevenLabs) a backlog can cost up to 96 times as much per day. On a paid or shared server, set TRANSCRIPTION_DRAIN_INTERVAL_MINUTES to 0 or to a larger interval, or lower TRANSCRIPTION_BACKFILL_PER_RUN.
  • Drains send at most one synchronous request at a time, or keep at most TRANSCRIPTION_BACKFILL_PER_RUN akou jobs open per account. Files the listener sends itself come on top of that.
  • A timeout or a server error on the OpenAI endpoint counts toward a file's three attempts. After a timer drain that ended on the server's side, the timer waits twice as long before the next one, up to a day, so an outage does not use up the attempts of many files. See Retries.
  • A drain with nothing to do reads the database only. It makes no request to the server and logs nothing at info level.
  • While the server cannot be reached and files wait, each drain logs one warning, up to 4 an hour at the default interval.
  • Only the drain after a full pass sends a missing file back to download. A timer drain stores file_missing, which does not count toward the three.
  • Set TRANSCRIPTION_DRAIN_INTERVAL_MINUTES to 0 to keep only the drain after each full pass. See When files are sent.

The one-shot backup command and the standalone listener do not change.

Picture sizes

Photos, videos and GIFs in the viewer take the size Telegram Desktop gives them, so most of them get larger and some bubbles get wider. No setting controls it and nothing in the archive changes. The size comes from the width and height the backup stored with each file. Releases before 7.32.0 stored no size for photos from the full pass. Those photos show a 4:3 box until the file loads, and then take their real shape, which moves the chat once. To store their size from each file's header, run telegram-archive check-media first. The dry run changes nothing and prints Photos without size with their count, next to the other problems it found. Then run telegram-archive check-media --repair. Its photo size step changes only photo rows with no size and never writes a file. The same run also makes every other repair check-media does: it puts files back from copies on disk, marks missing files and files cut short to download again, and marks files that are back at their path downloaded. Read the dry run's counts before you repair. It needs no Telegram session, so the backup can keep running. See check-media.

A photo imported from a Telegram Desktop HTML export before 9.3.0 was stored with the size of the export's thumbnail, not the photo's. The shape is right, but the viewer now draws the photo at that smaller size. check-media does not change it, because a repair never replaces a stored size. Photos imported from a JSON export, and photos the backup downloaded, are not affected.

Chats several accounts hold

Only an archive that holds more than one Telegram account looks different, and only for a login that can see more than one of them. A private chat between two of your archived accounts is drawn from each row's own account: its messages on the right, the other account's on the left, with no account name on the messages. Before, both rows showed every message on the right with an account name. In a group or channel several of your accounts hold, the account name beside the time is now in bold, in the colour of that account's tag. Nothing needs doing: no setting, no migration and no stored data changes.

Custom emoji

The first start runs migration 040. It adds the custom_emoji table and, for every custom emoji already stored as a reaction, a record marked as not fetched yet, dated when the archive first saw that emoji. It reads the reactions and their history once and changes no other table.

The next backup run then fetches the files of those emoji into media/_emoji, up to 500 per account, with one request per 100 emoji and a second between requests. A large archive with more custom emoji than that gets the rest over the next runs. Each telegram-archive backfill-details --apply run also fetches up to 500 more, so it can be repeated to get them sooner. The fetch does not follow DOWNLOAD_MEDIA: like profile photos, custom emoji are always fetched. See Custom emoji.

Custom emoji in message text are noted as messages are stored from this release on. For messages archived before, run telegram-archive backfill-details --apply once with the backup stopped: it finds the custom emoji in old texts and their earlier versions without asking Telegram for the messages, and fetches their files.

With the listener on, a custom emoji first seen live is fetched within about five minutes, without waiting for the daily run.

Logins whose downloads are off see custom emoji, as they see profile photos.

API clients

In the viewer's API and live frames, a custom emoji entity's document_id is now a string of digits, because a JSON number rounds it in a browser. A script or a bridge that reads raw_data.entities or message versions and compares document_id as a number must read it as a string. The chat exports keep the number. See Custom emoji.

Upgrading to 9.2.0

The real-time listener is on by default, and the full pass runs once a day. The listener saves new messages, their media, edits, chat actions and reactions as they happen. LISTEN_NEW_MESSAGES_MEDIA and LISTEN_REACTIONS now default to true as well. LISTEN_DELETIONS stays false, so the archive still keeps deleted messages untouched.

SCHEDULE now defaults to 0 3 * * * when the listener is on, and stays 0 */6 * * * when it is off. The time is the container's local time, UTC unless you set TZ.

Who sees a change:

  • An install that sets nothing: a pip install, a docker run without these variables, or a compose file without these keys. It starts a listener per account at the next start, downloads new media at once, and runs one full pass a day at 03:00 instead of every 6 hours.
  • An install that copied .env.example or docker-compose.yml from an older release. Those files set ENABLE_LISTENER=false and SCHEDULE=0 */6 * * * explicitly, so nothing changes. To move to the new setup, set ENABLE_LISTENER=true and remove the SCHEDULE line, or take the new .env.example and compose file.
  • An install that keeps an hourly or other frequent SCHEDULE with the listener on. It still works. Frequent full passes are deprecated with the listener on, and startup logs a note. With the listener off they stay a normal choice.

To keep the old behaviour, set:

ENABLE_LISTENER=false

SCHEDULE then falls back to every 6 hours on its own.

On SQLite, the listener sends live updates to the viewer over HTTP. A backup that runs without a viewer, such as a pip install that never starts viewer, now logs one warning when the first update cannot be delivered, and stays quiet after that until one gets through.

telegram-archive status now exits 1 when ENABLE_LISTENER is on and an account has no running listener. A setup that runs the one-shot backup command from a host cron runs no listener, so set ENABLE_LISTENER=false there.

A running listener now stamps a heartbeat in the database every 30 seconds, and the viewer counts a listener as running only while that stamp is fresh. Upgrade both images together: a 9.2.0 viewer beside an older backup reads every listener as not running, because the older backup writes no heartbeat.

The listener settings lines (ENABLE_LISTENER enabled, the LISTEN_* values, the EVENT_WEBHOOK lines) now appear only in the log of the backup service that runs schedule. The viewer and the one-shot commands no longer print them.

Files cut short

check-media now finds video and audio files that were cut short. A release from late 2025 stored some downloads that stopped early as complete files, and nothing in the archive knew. A dry run counts them as Cut short and exits 1, so a script or a health check that runs check-media reports them until they are repaired. Run check-media --repair once to mark them to download again. The next backup run downloads each one and replaces the short file only when its bytes are the start of the new download. Otherwise it keeps the short file and stores the new download beside it. See A file cut short.

The backup and the real-time listener now refuse a download shorter than the size Telegram declares for the file. The backup tries it again and then records it not downloaded, so the pending downloads retry it. Before, the short file was stored as complete.

Map pictures

New backups make one request to Telegram per location, venue and live location, once (a live location the backup reads again at a new position makes one more), and keep the map picture Telegram renders for it, a few tens of kilobytes, in the chat's media folder. DOWNLOAD_MEDIA=false and SKIP_MEDIA_CHAT_IDS turn it off as they do for every file. For locations archived before, run telegram-archive backfill-details --apply with the backup stopped. It fetches up to 500 pictures per run, one request each with a second between them, and says how many it deferred; run it again until none are deferred. See Fill old locations, contacts, polls and edit flags.

Stickers

Before 9.2.0 the backup archived every video sticker as a video, and some older rows hold animated stickers as a document. The first start runs migration 039. It changes only the type of those rows to sticker: no file, no file name and no id changes, and nothing is deleted. A row is changed only when its file carries the name Telegram gives a sticker: a video ending in _sticker.webm within Telegram's limits for a video sticker (512 px on each side and 3 seconds at most), or a document ending in _AnimatedSticker.tgs. The earlier media of edited messages get the same fix, so the edit history calls them Sticker too. It reads the media table and the earlier-media table once each.

What changes for those rows:

  • They play in the chat as stickers, and leave Photos & Videos and Files in the shared media gallery.
  • Replies, the pinned bar and the chat list call them Sticker, not Video or File.
  • Both chat exports give them the type sticker.
  • Transcription no longer picks them up. A video sticker has no sound.
  • DOWNLOAD_MEDIA_TYPES governs them as sticker. With a list that names video but not sticker, new video stickers are no longer downloaded. Files already on disk stay.

A file that had stickers drawn on it was archived as a sticker before 9.2.0. New ones are archived as the video, GIF or file they are. The migration leaves the old rows as they are: the row does not say whether the file was a video or a GIF. The chat shows them as the text "Sticker", a link that downloads the file when your login may download. A backup that reads such a message again corrects its type.

Upgrading to 9.0

Migrations

The first start runs migrations 034 to 038. 034, 035 and 036 add nullable columns (messages.edit_hide; source, entities and rich_message on message_versions; media.telegram_file_id) and one table, media_versions, and copy no data. 037 adds reaction_history and copies one baseline row per emoji from reactions: the last count each emoji had, and for an emoji taken back a second row with count 0. It changes no existing row. It reads reactions twice, so on a large archive the first start takes longer. Before 9.0, a count that dropped without reaching zero and an earlier removal of an emoji that came back were overwritten, so a message's history starts at its baseline. 038 adds message_snapshots and copies no data.

One-time steps

  • Run telegram-archive check-media, then telegram-archive check-media --repair if it reports anything. See Media files behind broken links.
  • With the backup stopped, run telegram-archive backfill-details --apply. A message archived before 9.0 keeps a pencil from a reaction-only edit until a backup reads it again, and an old location, contact or poll shows Details not archived. The command fills both from Telegram, at one request per 100 of those messages. See Fill old locations, contacts, polls and edit flags.

Chat exports

Both chat exports, the viewer's Export chat and telegram-archive export, change shape. A script that reads them must change:

  • message_versions is gone. Read the versions list on each message instead. An entry no longer carries chat_id and message_id: it belongs to the message it sits under. In the command's file that message has chat_id and account_id.
  • The window applies to the message, not to the version. The flat list picked versions by their own date. versions holds every version of each exported message, whatever its date. A version of a message sent outside the window is no longer in the file.
  • Each version is complete. It has text and date, as the flat list had, and, new in 9.0, captured_at, source, entities, rich_message and media.
  • Messages and versions list media. Each message has media and each version has media, as lists of media_id, type, file_name, file_size, mime_type, width, height and duration. A version that holds only earlier media has text null and media_only true. A script that rejects unknown keys must accept these.
  • total_message_versions counts what is under the messages. In the command's statistics it is the number of entries in every message's versions, media-only entries included.

Every transcript in a file now names a media listed in the same file. The fields are in Export and export.

Scripts and API clients

  • Poll and preview payloads. raw_data.poll and raw_data.webpage keep the first capture. Read the newest poll results or preview from snapshots in /api/chats/{ref}/messages or in either export. See Poll and link preview snapshots.
  • Empty formatting. Messages archived from now on store entities: [] in raw_data when they have no formatting, and an edit that removes all formatting leaves that list. [] means the message has no formatting. Messages archived before stay as they are, and a missing key on one of them still means the formatting was not recorded.
  • Live frames. An edit frame carries edit_hide, and media when the edit replaced the photo or file. The nested media of a new_message frame has the messages route's shape: the {message_id}_{type} key as id, a url, and no URL or path for a login whose downloads are off. See Live updates over WebSocket.
  • Transcript presses. For every login but the master, the transcript POST routes answer 429 with Retry-After past TRANSCRIPTION_ASK_RATE_LIMIT presses in 10 minutes (30 by default), or while TRANSCRIPTION_ASK_MAX_OPEN pressed files (50) wait for the backup. 0 turns either limit off. See Transcripts.

Operators

  • MASS_OPERATION_THRESHOLD counts deletions only. Edits are no longer limited. See Mass-operation protection.
  • The listener's stop statistics count an edit of a message it had not stored as Stored as new messages, no longer as Skipped.
  • DELETION_MODE=hard and deleting a chat also remove a message's snapshots, reaction history and earlier media. SKIP_MEDIA_DELETE_EXISTING also removes the earlier media an edit replaced.
  • The first transcription drains may send files that failed before only because their file was missing or the server failed. See Retries.

Media files behind broken links

Run telegram-archive check-media once after upgrading, and telegram-archive check-media --repair if it reports a broken link or a missing file. With the media folder missing, unreadable or empty, VERIFY_MEDIA skips its run with a warning instead of fetching every file again, and check-media exits 1 without checking. Archives that stored channel media before 4.0.5 can hold rows that say downloaded with no file behind them, and only check-media, VERIFY_MEDIA and the transcription drain look for them. A backup run without VERIFY_MEDIA now warns when it reads such a row, with a count only. See A missing shared file.

Upgrading to 8.17.0

The package is renamed telegram_archive. See the module rename. Releases are also published on PyPI, and voice transcription supports more providers. Migration 033 runs on start. Nothing is needed beyond the routine upgrade.

Upgrading from 7.x

The jump from 7.x to 8.x has one irreversible step: migration 022. It rewrites the nine tables that hold captured data and records the account that captured each row. The tables hold chats, messages, media, reactions, earlier message versions, sync state, forum topics, folders and folder membership. After that one archive can hold several Telegram accounts. Media files do not move. Only the database changes.

Use 8.0.1 or later for this jump, never 8.0.0. 8.0.0 fails on some older archives that still hold messages from deleted chats. 8.0.1 fixes this. Going straight to the current release runs the same migrations, then every later one. One of them builds the full-text search index over your existing messages.

On PostgreSQL these migrations also create the pg_trgm extension. Give the database user permission to run CREATE EXTENSION, or have an administrator create the extension first.

  1. Stop both containers. On SQLite, migration 022 refuses to run while another process holds the database file open, and that includes the viewer.

    docker compose stop telegram-backup telegram-viewer
    
  2. Back up the database. This is the only way back. Backing up the archive has the commands for SQLite and PostgreSQL.

  3. On SQLite, check that free space on the same filesystem is at least three times the size of the database file plus its -wal file, if one exists. Migration 022 checks this before it writes anything.

  4. Move both image pins to the new release, as in the routine upgrade.

  5. Start the backup first and wait for its migrations to finish. Then start the viewer.

    docker compose up -d telegram-backup
    docker compose logs -f telegram-backup
    

    When the log shows that the migrations are done, start the viewer:

    docker compose up -d telegram-viewer
    

If there is not enough free space, or another process holds the file, migration 022 stops with a message that ends in Nothing has been changed. Fix the cause and start the backup container again.

During migration 022:

  • On SQLite the database peaks at a little under 2.4 times its old size.
  • PostgreSQL barely grows.
  • Everything you already have becomes account 1. A single-account install keeps running with no new settings.

From 8.0 on, the viewer addresses each chat by its chat ref instead of the Telegram chat id. Old bookmarks to chats stop working. Open the chat from the sidebar to get its new address. Share links keep working: the link carries only the token, and migration 022 converts each token's chat grant to the new form. Scripts that create viewer accounts or share tokens through the admin API must send allowed_chat_refs instead of allowed_chat_ids. The API rejects the old field with HTTP 400.

Older upgrades

Jumps from older major versions, such as v3 to v4 or v4 to v5, are described at the end of the changelog.