How qBittorrent Saves and Loads Torrent Resume Data: Architecture and Implementation

qBittorrent persists torrent states using either legacy .fastresume files or an experimental SQLite database, both managed by the ResumeDataStorage abstraction in src/base/bittorrent/.

qBittorrent maintains download progress, file priorities, and share limits across application restarts through a robust resume data system. The architecture centers on an abstract ResumeDataStorage class with two concrete implementations: legacy file-based storage and modern SQLite storage. This article examines the internal mechanisms that handle torrent state persistence according to the qbittorrent/qBittorrent source code.

Resume Data Storage Architecture

The Abstract ResumeDataStorage Interface

All persistence operations flow through the abstract base class defined in src/base/bittorrent/resumedatastorage.h. This interface declares pure virtual methods doLoadAll(), load(), and store() that concrete implementations must override. The base class handles thread lifecycle management and emits signals like loadStarted and loadFinished to notify the SessionImpl of asynchronous operation progress.

Legacy vs SQLite Implementations

The application supports two storage backends selected via Advanced → Network → Resume data storage type:

  • BencodeResumeDataStorage: Stores individual .fastresume files per torrent in the dataPath/fastresume directory, maintaining original qBittorrent behavior.
  • DBResumeDataStorage: Persists all data in a single SQLite file (resume.db) with schema version 10, offering atomic updates and reduced filesystem clutter.

In src/base/bittorrent/sessionimpl.cpp, the SessionImpl::init() method instantiates the appropriate storage based on the ResumeDataStorageType configuration:

if (type == ResumeDataStorageType::SQLite)
    m_resumeDataStorage = new DBResumeDataStorage(dbPath, this);
else
    m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this);

How Resume Data Is Loaded on Startup

Initialization and Asynchronous Loading

When qBittorrent launches, SessionImpl triggers ResumeDataStorage::loadAll(), which spawns a background thread calling the virtual doLoadAll(). The legacy implementation scans the fastresume directory, while the SQLite version queries the torrents table, emitting loadStarted with the list of torrent IDs before processing begins.

Parsing libtorrent Resume Blobs

Each storage implementation parses raw bencoded data using libtorrent APIs. In DBResumeDataStorage::parseQueryResultRow (found in src/base/bittorrent/dbresumedatastorage.cpp), the code decodes the binary blob:

const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec, nullptr,
                                                  bdecodeDepthLimit, bdecodeTokenLimit);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(resumeDataRoot, ec);

The BencodeResumeDataStorage::loadTorrentResumeData method performs equivalent parsing for file-based storage, converting the bencoded entry into lt::add_torrent_params for libtorrent session injection.

How Resume Data Is Saved

Store Operations and Threading

State changes trigger ResumeDataStorage::store(const TorrentID&, const LoadTorrentParams&). The legacy backend writes .fastresume files synchronously on a dedicated I/O thread (m_ioThread), while the SQLite backend enqueues StoreJob instances to an asynchronous worker thread (DBResumeDataStorage::Worker).

The SQLite worker serializes parameters using lt::write_resume_data():

lt::entry data = lt::write_resume_data(p);
// ... bencode data into QByteArray ...
query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData);
if (!bencodedMetadata.isEmpty())
    query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
query.exec();

Metadata extraction occurs similarly in both backends, though SQLite stores it in a dedicated column while legacy storage uses separate .metadata files.

Queue Position Persistence

Torrent queue order is preserved through the storeQueue(const QList<TorrentID>&) method. For SQLite, this creates a StoreQueueJob that updates the queue_position column for each torrent ID. The legacy implementation updates the queue_position field within each respective .fastresume file.

Practical Code Examples

Loading All Torrents at Startup

// Assume `session` is a BitTorrent::SessionImpl pointer
session->resumeDataStorage()->loadAll();   // asynchronous, emits signals

connect(session->resumeDataStorage(), &BitTorrent::ResumeDataStorage::loadStarted,
        this, [](const QList<BitTorrent::TorrentID> &ids){
    qDebug() << "Loading resume data for" << ids.size() << "torrents";
});
connect(session->resumeDataStorage(), &BitTorrent::ResumeDataStorage::loadFinished,
        this, [](){ qDebug() << "All resume data loaded"; });

Storing a Single Torrent's State

BitTorrent::Torrent *t = session->torrentHandle(torrentID);
LoadTorrentParams resume = t->getResumeData();   // collects libtorrent params + UI fields
session->resumeDataStorage()->store(torrentID, resume);

Updating Queue Order

QList<BitTorrent::TorrentID> newQueue = session->queuedTorrentIDs();
session->resumeDataStorage()->storeQueue(newQueue);

Summary

  • qBittorrent uses an abstract ResumeDataStorage class with two backends: legacy .fastresume files (BencodeResumeDataStorage) and SQLite (DBResumeDataStorage).
  • Storage selection occurs in SessionImpl::init() via the ResumeDataStorageType enum value.
  • Resume data loading runs asynchronously in background threads, parsing bencoded blobs through lt::bdecode and lt::read_resume_data.
  • Saving occurs via store() methods that use lt::write_resume_data, with SQLite operations queued to a dedicated worker thread.
  • Queue positions are stored separately through storeQueue(), mapping to either database columns or file fields.

Frequently Asked Questions

Where does qBittorrent store resume data files?

Legacy storage places individual .fastresume files in the fastresume subdirectory of the application data path, while the experimental SQLite backend consolidates everything into a single resume.db file. Metadata for hybrid torrents may be stored in separate .metadata files when using the legacy backend, or in the metadata BLOB column when using SQLite.

What is the difference between BencodeResumeDataStorage and DBResumeDataStorage?

BencodeResumeDataStorage maintains one file per torrent using bencoded dictionaries, performing I/O on a dedicated QThread. DBResumeDataStorage uses a SQLite database with schema version 10, offering atomic transactions and asynchronous writes through a custom Worker thread class. The SQLite approach reduces filesystem fragmentation and supports faster batch updates.

How does qBittorrent handle resume data corruption?

Both implementations rely on libtorrent's bdecode with explicit depth and token limits (bdecodeDepthLimit, bdecodeTokenLimit) to prevent malicious or corrupted data from causing stack exhaustion. The SQLite backend benefits from database integrity checks, while the legacy loader validates file existence and parsing errors before emitting loaded torrents.

Can I migrate between legacy and SQLite resume storage?

qBittorrent does not provide automatic migration between storage formats. Switching modes in Advanced → Network begins using the new format for subsequent operations, but existing data remains in the original format. Users must re-add torrents to populate the new storage type, or manually convert data using external tools that understand both the .fastresume bencode structure and the SQLite schema.

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 →