# How Motrix Persists State Between Sessions: JSON Settings and SQLite Task Storage

> Discover how Motrix persists state using JSON settings and SQLite task storage. Learn about its robust dual-layer strategy for uninterrupted downloads.

- Repository: [Dr_rOot/Motrix](https://github.com/agalwood/Motrix)
- Tags: internals
- Published: 2026-08-20

---

**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`](https://github.com/agalwood/Motrix/blob/main/src/core/settings/settings-manager.ts) and [`src/core/session/motrix-database.ts`](https://github.com/agalwood/Motrix/blob/main/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`](https://github.com/agalwood/Motrix/blob/main/settings.json), preserving critical credentials and download paths.

```typescript
// 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`](https://github.com/agalwood/Motrix/blob/main/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.

```typescript
// 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`](https://github.com/agalwood/Motrix/blob/main/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:

1. **Hydration**: Loads all persisted tasks from SQLite via `this.db.getAllTasks()`.
2. **Live Query**: Fetches current Aria2 engine state through `tellActive`, `tellWaiting`, and `tellStopped`.
3. **Reconciliation**: Merges database snapshots with live GIDs, handling duplicates and lost identifiers.
4. **Adoption**: Re-adds tasks existing in the database but missing from the engine, immediately persisting status changes via `persistTask` or `persistTaskWithOccurrence`.

## 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`](https://github.com/agalwood/Motrix/blob/main/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 `SettingsManager` uses `write-file-atomic` to guarantee that [`settings.json`](https://github.com/agalwood/Motrix/blob/main/settings.json) updates are crash-proof, protecting RPC secrets and configuration data.
- **Transactional SQL storage**: `MotrixDatabase` wraps `better-sqlite3` with 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 `saveTasksBatch` prevents 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`](https://github.com/agalwood/Motrix/blob/main/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`](https://github.com/agalwood/Motrix/blob/main/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`](https://github.com/agalwood/Motrix/blob/main/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`](https://github.com/agalwood/Motrix/blob/main/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.