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

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)

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 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)

Located in 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, the core process instantiates and starts both threads after loading the configuration:

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:

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:

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.

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 →