# How Frigate Handles Event Cleanup and Recording Maintenance: A Deep Dive into the Source Code

> Discover how Frigate handles event cleanup and recording maintenance. Learn about its automated storage management and retention policies directly from the source code.

- Repository: [Blake Blackshear/frigate](https://github.com/blakeblackshear/frigate)
- Tags: deep-dive
- Published: 2026-05-25

---

**Frigate automates storage management through two background threads—`EventCleanup` and `RecordingCleanup`—that periodically purge expired clips, snapshots, and recordings based on retention policies defined in your configuration.**

The `blakeblackshear/frigate` repository implements a sophisticated cleanup pipeline to prevent unbounded storage growth. Instead of relying on external cron jobs, the application spawns dedicated worker threads that interact directly with the SQLite database and filesystem, ensuring that orphaned media files and stale metadata are removed efficiently and safely.

## The EventCleanup Thread ([`frigate/events/cleanup.py`](https://github.com/blakeblackshear/frigate/blob/main/frigate/events/cleanup.py))

The `EventCleanup` class runs as a background thread responsible for pruning event metadata, thumbnails, and associated media once retention periods expire. It operates in [`frigate/events/cleanup.py`](https://github.com/blakeblackshear/frigate/blob/main/frigate/events/cleanup.py) and sleeps for **5 minutes** between cycles unless the instance is in safe mode.

### Expiring Clips and Database Flags

The thread first calculates a global `expire_days` value from the `record` configuration. It then queries the database for events older than this threshold and performs two critical actions:

1. **Deletes the underlying clip files** from storage.
2. **Clears the `has_clip` flag** in the `Event` table to mark the resource as removed.

This incremental approach processes events in chunks to maintain responsiveness on large installations.

### Snapshot Lifecycle Management

Next, the thread handles image retention using per-camera or global `snapshots.retain` settings. The `expire_snapshots()` method locates stale snapshots, removes the image files via `delete_event_snapshot()`, and updates the `has_snapshot` boolean to `False` in the database. The cleanup works in batches of **50 events** to avoid locking the database for extended periods.

### Timeline and Vector Cleanup

Once clips are expired, the thread removes any `Timeline` rows that reference deleted recordings. When both `has_clip` and `has_snapshot` flags are `False` for an event, the entire event row is purged from the database, including any embedded semantic-search vectors stored in the SQLite-vec extension.

## The RecordingCleanup Thread ([`frigate/record/cleanup.py`](https://github.com/blakeblackshear/frigate/blob/main/frigate/record/cleanup.py))

Located in [`frigate/record/cleanup.py`](https://github.com/blakeblackshear/frigate/blob/main/frigate/record/cleanup.py), the `RecordingCleanup` thread manages the underlying video segments, preview files, and database WAL maintenance. It wakes every **60 seconds**, performing lightweight checks continuously and executing the heavy-weight `expire_recordings()` pass only once per `record.expire_interval` (default **1 hour**).

### Temporary File Cleanup and WAL Truncation

Each cycle begins by scanning cache directories for transient media:

- **Tmp-preview removal**: Deletes `preview_*.mp4` files older than 1 hour.
- **Tmp-clip removal**: Deletes `clip_*.mp4` files older than 1 hour.
- **WAL truncation**: If the SQLite write-ahead log exceeds `MAX_WAL_SIZE` (approximately 200 MiB), the thread executes `PRAGMA wal_checkpoint(TRUNCATE)` to reclaim disk space.

### Recording and Review Segment Expiry

For every camera, the thread calculates `alert` and `detection` expiration timestamps. It then:

1. Deletes expired thumbnail files associated with `ReviewSegment` rows.
2. Removes the corresponding `ReviewSegment` and `UserReviewStatus` entries from the database.
3. Builds cutoff timestamps for continuous and motion recordings, then walks the `Recordings` table. If a segment falls before the cutoff and is not referenced by any active review, the file is unlinked and the database row is deleted in chunks of up to **100,000 rows**.

### Preview File and Empty Directory Cleanup

After processing recordings, the thread identifies preview files that no longer overlap with retained recordings and removes them. Finally, it invokes `remove_empty_directories()` to prune any directories that have become empty during the cleanup process.

## Configuration and Retention Policies

Both threads respect settings defined in your Frigate configuration file:

| Setting | Location | Effect |
|---------|----------|--------|
| `record.alerts.retain.days` / `record.detections.retain.days` | Global | Determines the maximum age for clip files processed by `expire_clips()`. |
| `snapshots.retain.default` / `snapshots.retain.objects` | Global or per-camera | Controls how long snapshot images are kept before deletion. |
| `record.expire_interval` | Global (hours) | Controls how often `RecordingCleanup` performs the full recording expiry pass. |
| `safe_mode` | Global | When `True`, both cleanup threads skip all operations, useful for debugging database issues. |

## Code Examples

### Starting the Cleanup Threads (Internal Initialization)

As implemented in [`frigate/main.py`](https://github.com/blakeblackshear/frigate/blob/main/frigate/main.py), the core process instantiates and starts both threads after loading the configuration:

```python
from multiprocessing import Event as MpEvent
from frigate.events.cleanup import EventCleanup
from frigate.record.cleanup import RecordingCleanup

# Shared shutdown signal

stop_event = MpEvent()

# Initialize cleaners

event_cleanup = EventCleanup(config, stop_event, db)
record_cleanup = RecordingCleanup(config, stop_event)

# Start background threads

event_cleanup.start()
record_cleanup.start()

```

### Manually Triggering a One-Off Cleanup Cycle

For maintenance scripts or testing, you can instantiate the classes and call their public methods directly:

```python
from multiprocessing import Event as MpEvent
from frigate.events.cleanup import EventCleanup
from frigate.record.cleanup import RecordingCleanup

stop_event = MpEvent()
config = ...  # FrigateConfig instance

db = ...      # SqliteVecQueueDatabase instance

# Event cleanup

event_cleaner = EventCleanup(config, stop_event, db)
event_cleaner.expire_clips()
event_cleaner.expire_snapshots()

# Recording cleanup

record_cleaner = RecordingCleanup(config, stop_event)
record_cleaner.clean_tmp_previews()
record_cleaner.clean_tmp_clips()
record_cleaner.expire_recordings()

```

### Simulating Cleanup Operations (Dry Run)

To verify what would be deleted without removing files, use the `sync_recordings` utility with the `dry_run` flag:

```python
from frigate.util.media import sync_recordings

result = sync_recordings(dry_run=True, force=False)
print(f"Orphans found: {result.orphans_found}")
print(f"Files to delete: {result.deleted_files}")

```

## Summary

- **Dual-thread architecture**: Frigate uses `EventCleanup` (5-minute cycles) for metadata and snapshots, and `RecordingCleanup` (1-minute cycles) for video segments and WAL maintenance.
- **Incremental processing**: Deletions occur in chunks (50 events for snapshots, 100,000 rows for recordings) to prevent performance degradation.
- **Safe mode compatibility**: Setting `safe_mode: True` immediately disables all background cleanup operations without requiring a code change.
- **Atomic cleanup**: The system removes database rows only after confirming file deletion, and purges empty directories to prevent filesystem clutter.
- **Configurable granularity**: Retention policies can be set globally or per-camera for both snapshots and recordings.

## Frequently Asked Questions

### How often does Frigate run event cleanup and recording maintenance?

The `EventCleanup` thread wakes every **5 minutes** to process expired clips and snapshots. The `RecordingCleanup` thread runs every **60 seconds** for lightweight tasks like temporary file removal, but executes the full recording expiry logic only once per `record.expire_interval`, which defaults to **1 hour**.

### What happens if I enable `safe_mode` in the configuration?

When `safe_mode` is set to `True`, both the `EventCleanup` and `RecordingCleanup` threads detect this flag at the start of their loops and skip all deletion operations. This allows administrators to debug database issues or recover files without background processes interfering.

### Can I run cleanup manually without restarting Frigate?

Yes. You can import the cleanup classes from `frigate.events.cleanup` and `frigate.record.cleanup` into a Python script or interactive shell, instantiate them with the running configuration and database connection, and call methods like `expire_clips()` or `expire_recordings()` directly. This is useful for emergency space reclamation or testing retention policies.

### Does Frigate delete recordings that are part of an active review?

No. The `RecordingCleanup` logic specifically checks for active reviews before deleting segments. If a recording is referenced by a `ReviewSegment` that has not yet expired according to the `alerts.retain.days` or `detections.retain.days` settings, the file and database row are preserved until the review period lapses.