How the Self-Destruct Messaging Feature Works in Telegram Desktop: A Technical Deep Dive

The self-destruct messaging feature in Telegram Desktop implements Time-To-Live (TTL) through three coordinated subsystems: an API layer for server configuration, a HistoryServiceSelfDestruct component attached to individual messages, and a central Session::_selfDestructTimer that monitors expiration and triggers removal when getSelfDestructIn() returns zero.

Telegram Desktop's self-destruct messaging feature enables users to send photos and videos that automatically disappear after a specified duration. According to the telegramdesktop/tdesktop source code, this TTL system relies on a tight integration between the MTProto API layer, message-level components, and session-wide timers to ensure synchronized expiration across clients.

Configuring TTL via the API Layer

User-facing configuration begins in the settings dialogs. The Settings UI (settings/sections/settings_privacy_security.cpp and settings/sections/settings_global_ttl.cpp) provides interfaces for selecting default message TTL and account self-destruction intervals.

When a user modifies these values, the application invokes the API wrapper defined in api/api_self_destruct.h:

session->api().selfDestruct().updateDefaultHistoryTTL(ttl);   // settings_global_ttl.cpp
session->api().selfDestruct().updateAccountTTL(days);        // settings_privacy_security.cpp

The implementation in api/api_self_destruct.cpp transmits the corresponding MTProto requests (messages_SetDefaultHistoryTTL and account_SetAccountTTL) and stores the server response in reactive variables (_defaultHistoryTTL.period and _accountTTL.days). This ensures the client maintains the authoritative TTL values for subsequent message operations.

Attaching Self-Destruct Components to Messages

When composing a self-destructing media message, the client attaches a specialized component to the HistoryItem. The method HistoryItem::setSelfDestruct() in history/history_item.cpp (lines 7296–7307) initializes this data:

void HistoryItem::setSelfDestruct(
        HistorySelfDestructType type,
        MTPint mtpTTLvalue) {
    UpdateComponents(HistoryServiceSelfDestruct::Bit());
    const auto selfdestruct = Get<HistoryServiceSelfDestruct>();
    if (mtpTTLvalue.v == TimeId(0x7FFFFFFF)) {
        selfdestruct->timeToLive = TimeToLiveSingleView();   // “forever”
    } else {
        selfdestruct->timeToLive = mtpTTLvalue.v * crl::time(1000);
    }
    selfdestruct->type = type;
}

The component structure is defined in history/history_item_components.h (lines 28–35):

struct HistoryServiceSelfDestruct
    : RuntimeComponent<HistoryServiceSelfDestruct, HistoryItem> {
    using Type = HistorySelfDestructType;
    Type type = Type::Photo;
    std::variant<crl::time, TimeToLiveSingleView> timeToLive = crl::time();
    std::variant<crl::time, TimeToLiveSingleView> destructAt = crl::time();
};

This component stores both the timeToLive duration and the calculated destructAt timestamp. When the server echoes the message to recipients, they receive the same destructAt value, ensuring consistent expiration across all clients.

The Session Timer and Expiration Tracking

All pending self-destruct messages are tracked centrally by the Session class in data/data_session.cpp. The system maintains a vector Session::_selfDestructItems containing FullMsgId identifiers and a single base::Timer instance called _selfDestructTimer.

When a new self-destructing message is created, the system registers it via Session::selfDestructIn() (lines 3201–3206):

void Session::selfDestructIn(not_null<HistoryItem*> item, crl::time delay) {
    _selfDestructItems.push_back(item->fullId());
    if (!_selfDestructTimer.isActive()
        || _selfDestructTimer.remainingTime() > delay) {
        _selfDestructTimer.callOnce(delay);
    }
}

The timer callback Session::checkSelfDestructItems() (lines 3210–3229) processes the queue:

void Session::checkSelfDestructItems() {
    const auto now = crl::now();
    crl::time nextDestructIn = 0;
    for (auto i = _selfDestructItems.begin(); i != _selfDestructItems.cend();) {
        if (const auto item = message(*i)) {
            if (const auto destructIn = item->getSelfDestructIn(now)) {
                if (nextDestructIn > 0) {
                    accumulate_min(nextDestructIn, destructIn);
                } else {
                    nextDestructIn = destructIn;
                }
                ++i;
            } else {
                i = _selfDestructItems.erase(i);
            }
        } else {
            i = _selfDestructItems.erase(i);
        }
    }
    if (nextDestructIn > 0) {
        _selfDestructTimer.callOnce(nextDestructIn);
    }
}

This implementation efficiently batches expiration checks by rescheduling the timer only for the nearest future expiry, minimizing CPU wakeups.

Computing Message Lifetime

Individual messages calculate their remaining lifespan through HistoryItem::getSelfDestructIn() in history/history_item.cpp (lines 7830–7850):

crl::time HistoryItem::getSelfDestructIn(crl::time now) {
    if (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {
        const auto at = std::get_if<crl::time>(&selfdestruct->destructAt);
        if (at && (*at) > 0) {
            const auto destruct = *at;
            if (destruct <= now) {
                // Message already expired – replace service text with “expired”
                setServiceText({ TextWithEntities{ .text = expiredMessage() } });
                return 0;
            }
            return destruct - now;               // ms remaining
        }
    }
    return 0;
}

When this method returns zero, the calling code in checkSelfDestructItems() removes the message from the tracking vector. Simultaneously, the UI updates the message bubble to display a localized "photo expired" or "video expired" string via setServiceText().

UI Implementation and Countdown Display

The visual countdown is rendered using components in ui/effects/ttl_icon.cpp, which draws the timer badge next to the message bubble. Human-readable time formatting is handled by Ui::FormatTTL() in ui/text/format_values.cpp (lines 438–470), converting millisecond deltas into strings like "5 min" or "30 sec".

The TTL selection dialog (boxes/self_destruction_box.cpp, lines 148–173) provides the interface for choosing expiration intervals, invoking the same Api::SelfDestruct methods used in the settings panels.

Key Source Files

Component File Path Purpose
API Interface api/api_self_destruct.h Declares SelfDestruct class with updateDefaultHistoryTTL
API Implementation api/api_self_destruct.cpp MTProto request construction and reactive state storage
Session Timer data/data_session.cpp selfDestructIn and checkSelfDestructItems logic
Message Component history/history_item_components.h HistoryServiceSelfDestruct struct definition
TTL Logic history/history_item.cpp setSelfDestruct and getSelfDestructIn implementations
Settings UI settings/sections/settings_global_ttl.cpp Default TTL configuration interface
Formatting ui/text/format_values.cpp FormatTTL helper for countdown strings
Visual Effects ui/effects/ttl_icon.cpp Timer icon rendering

Summary

  • Configuration: The Api::SelfDestruct class manages server communication for both default message TTL and account self-destruction settings.
  • Message Tagging: The HistoryServiceSelfDestruct component attaches expiration metadata to HistoryItem objects, storing both timeToLive duration and absolute destructAt timestamps.
  • Centralized Tracking: Session::_selfDestructTimer monitors all pending expirations through the _selfDestructItems vector, using efficient batch processing to minimize resource usage.
  • Expiration Logic: HistoryItem::getSelfDestructIn() calculates remaining milliseconds and triggers UI updates when deadlines pass.
  • Visual Feedback: The UI layer combines ttl_icon.cpp for graphics and format_values.cpp for human-readable countdown text.

Frequently Asked Questions

What triggers a self-destruct message to disappear in Telegram Desktop?

The disappearance is triggered by Session::checkSelfDestructItems() when it detects that HistoryItem::getSelfDestructIn() returns zero, indicating the current time has exceeded the destructAt timestamp. At this point, the message is removed from the tracking list and its service text is replaced with an expiration notice.

How does Telegram Desktop handle "View Once" or infinite TTL settings?

When the server specifies a TTL value of 0x7FFFFFFF, the client stores a TimeToLiveSingleView() variant instead of a numeric timestamp in the HistoryServiceSelfDestruct component. This special value indicates the content should remain until manually viewed once, rather than expiring after a fixed duration.

What is the difference between default history TTL and account TTL?

Default history TTL (updateDefaultHistoryTTL) sets the automatic expiration timer for new messages sent in chats, while account TTL (updateAccountTTL) specifies how long the account itself should remain inactive before Telegram automatically deletes it. Both are managed through the api_self_destruct module but control distinct server-side behaviors.

Which source files contain the core logic for message expiration?

The expiration engine spans history/history_item.cpp (message-level TTL calculations), data/data_session.cpp (centralized timer and tracking), and history/history_item_components.h (data structures). UI components reside in ui/effects/ttl_icon.cpp and ui/text/format_values.cpp.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →