How Motrix Persists State Between Sessions: JSON Settings and SQLite Task Storage
Motrix uses a dual-layer persistence strategy that stores application settings in an atomic JSON file and download task metadata in a SQLite database with WAL mode, ensuring no data loss during crashes or restarts.
Motrix is an open-source download manager built on Electron that maintains a deterministic view of your downloads across application restarts. Understanding how Motrix persists state between sessions reveals a sophisticated architecture that couples atomic file writes for user preferences with transactional SQL storage for complex task graphs. This deep dive into the agalwood/Motrix repository examines the exact mechanisms—found in src/core/settings/settings-manager.ts and src/core/session/motrix-database.ts—that protect your RPC secrets, download progress, and notification history from corruption.
Application Settings Persistence via SettingsManager
The SettingsManager class handles all user preferences, from UI themes and download folders to RPC secrets and proxy configurations. Unlike casual localStorage implementations common in Electron apps, Motrix treats settings durability as a critical systems concern.
Loading and Schema Validation
When Motrix starts, SettingsManager.load() (lines 61-71) reads the JSON configuration file located at <user-data>/settings.json. The method validates incoming data against Zod schemas, migrates legacy settings formats, and injects runtime defaults such as auto-generated RPC secrets. This validation layer ensures that corrupted files or outdated configuration formats cannot crash the application on startup.
Atomic Write Protection
Settings modifications trigger saveSettings(), which eliminates corruption risks using the write-file-atomic package. The implementation (lines 18-21) writes to a temporary file with a random suffix, flushes buffers to disk via fsync, then performs an atomic rename over the target. This guarantees that a crash during the write operation cannot truncate your settings.json, preserving critical credentials and download paths.
// src/core/settings/settings-manager.ts (excerpt)
private async saveSettings(settings: AppSettings): Promise<void> {
const dir = path.dirname(this.filePath);
await mkdir(dir, { recursive: true });
// Atomic: writes to <path>.<rand>, fsyncs, renames over the target.
await writeFileAtomic(this.filePath, JSON.stringify(settings, null, 2), {
encoding: 'utf-8',
});
}
Task and Notification Persistence in SQLite
While settings live in JSON, mutable download state—including task metadata, file selections, terminal occurrences, and diagnostic records—resides in a SQLite database (motrix.db). The MotrixDatabase class in src/core/session/motrix-database.ts wraps better-sqlite3 to provide transactional guarantees for complex relational data.
Database Architecture with WAL Mode
The constructor enables Write-Ahead Logging (WAL) via pragma('journal_mode = WAL') (lines 40-45). WAL mode allows concurrent reads during writes and prevents database corruption if the Electron process terminates unexpectedly, unlike traditional rollback journal modes that lock the entire database during transactions.
Optimized Batch Writes with Row Signatures
To minimize disk I/O during periodic saves, Motrix implements content-based deduplication. Before writing, saveTasksBatch computes a rowSignature hash of the task-instance graph. If the signature matches lastPersistedSig (cached from the previous write), the row is skipped (lines 45-48). This optimization prevents redundant SQLite transactions when task metadata remains unchanged between 50-millisecond save intervals.
Transactional Consistency for Complex Objects
When persisting tasks with terminal occurrences, persistTaskWithOccurrence (lines 7-15) wraps multiple operations in a single db.transaction block. This ensures that a task row and its associated occurrence records commit atomically—never partially. If the transaction fails, rollback leaves the database in its previous consistent state without orphaned records.
// src/core/session/motrix-database.ts (excerpt)
persistTaskWithOccurrence(payload: TaskWithInstances, occurrence: TaskOccurrence | null): void {
this.db.transaction(() => {
this.writeRow(payload);
if (occurrence) {
this.stmtInsertOccurrenceOrIgnore.run(this.serializeOccurrence(occurrence));
}
this.lastPersistedSig.set(payload.task.motrixId, this.rowSignature(payload));
})();
}
Session Restoration on Startup
The SessionManager.restore() method in src/core/session/session-manager.ts (starting at line 86) orchestrates the transition from persisted storage to active downloads. The restoration process executes four distinct phases:
- Hydration: Loads all persisted tasks from SQLite via
this.db.getAllTasks(). - Live Query: Fetches current Aria2 engine state through
tellActive,tellWaiting, andtellStopped. - Reconciliation: Merges database snapshots with live GIDs, handling duplicates and lost identifiers.
- Adoption: Re-adds tasks existing in the database but missing from the engine, immediately persisting status changes via
persistTaskorpersistTaskWithOccurrence.
Debounced Auto-Save Strategy
User interactions trigger SessionManager.requestSave(), which implements a 50-millisecond debounce window (lines 77-104). Multiple rapid changes—such as progress updates during high-speed downloads—collapse into a single SQLite transaction, reducing SSD wear while maintaining durability. This mechanism balances real-time consistency with I/O performance, ensuring Motrix does not thrash the disk during batch operations.
Notification Ledger Design
Motrix stores notifications using a dual-table strategy within motrix-database.ts. The notification_occurrences table serves as an immutable ledger for idempotent delivery guarantees, while the notifications display table maintains a capped history of 500 rows. The insertNotificationWithLedger method (lines 75-99) writes both records in a single transaction. If the display insert fails due to duplicate keys or cap-pruning that removes the just-added row, the entire transaction rolls back, preventing "ghost" entries where a notification is recorded but not displayed.
Summary
- Dual-layer architecture: Motrix separates static settings (JSON) from dynamic task state (SQLite) to optimize for different access patterns and durability requirements.
- Atomic file writes: The
SettingsManageruseswrite-file-atomicto guarantee thatsettings.jsonupdates are crash-proof, protecting RPC secrets and configuration data. - Transactional SQL storage:
MotrixDatabasewrapsbetter-sqlite3with WAL mode and explicit transactions, ensuring task metadata, file selections, and terminal occurrences remain consistent even during unexpected shutdowns. - Intelligent deduplication: Row signature caching in
saveTasksBatchprevents unnecessary disk writes when task state remains unchanged between save cycles. - Debounced persistence:
SessionManager.requestSave()collapses rapid changes into 50ms windows, balancing immediate durability with I/O efficiency. - Reconciliation on startup: The
restore()method merges persisted database state with live Aria2 engine data, handling orphaned tasks and duplicate GIDs automatically.
Frequently Asked Questions
Where does Motrix store its configuration files?
Motrix stores application settings in a JSON file at <user-data>/settings.json, managed by SettingsManager in src/core/settings/settings-manager.ts. Download tasks, file metadata, and notification history reside in a SQLite database named motrix.db in the same user data directory, handled by MotrixDatabase in src/core/session/motrix-database.ts.
How does Motrix prevent settings corruption during crashes?
The saveSettings() method in SettingsManager uses the write-file-atomic npm package to perform atomic writes. It writes to a temporary file with a random suffix, flushes buffers to disk with fsync, then renames the temporary file over the target location. This ensures that the original settings.json remains intact if the Electron process crashes during the write operation.
What database engine does Motrix use for task persistence?
Motrix uses SQLite via the better-sqlite3 package, with Write-Ahead Logging (WAL) mode enabled in MotrixDatabase (lines 40-45). WAL mode allows readers to operate without blocking writers and prevents database corruption during power failures or application crashes, unlike traditional rollback journal modes.
How does Motrix handle session restoration after restart?
The SessionManager.restore() method in src/core/session/session-manager.ts rebuilds the in-memory task list by first loading all persisted tasks from SQLite, then querying the live Aria2 engine for active downloads. It reconciles differences between the database snapshot and live state, re-adopting tasks that exist in the database but not in the engine, and updates the storage immediately with any discovered changes.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →