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:
- Deletes the underlying clip files from storage.
- Clears the
has_clipflag in theEventtable 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_*.mp4files older than 1 hour. - Tmp-clip removal: Deletes
clip_*.mp4files older than 1 hour. - WAL truncation: If the SQLite write-ahead log exceeds
MAX_WAL_SIZE(approximately 200 MiB), the thread executesPRAGMA 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:
- Deletes expired thumbnail files associated with
ReviewSegmentrows. - Removes the corresponding
ReviewSegmentandUserReviewStatusentries from the database. - Builds cutoff timestamps for continuous and motion recordings, then walks the
Recordingstable. 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, andRecordingCleanup(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: Trueimmediately 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →