Transparent SSD caching for any read-heavy file workload — media servers, NAS-backed applications, anything that reads from slow storage. Sits between your application and a network share / drive array using a FUSE overmount — your application sees no difference, but files are silently pre-copied to local SSD before they're needed.
No plugins, no API wrappers, no config changes on the application side. Drop it in, point it at your media directory, and remove it just as easily. As long as it receives a proper signal, you can stop it while your server is running. Active streams will need to be restarted, but your server won't.
I created this for a few reasons:
- Works on top of any filesystem. This is the main motivation. I have MergerFS + Snapraid and some LVM. A lot of times, I just want to add a small file cache on top of my most hotly contested files without needing to change the entire setup.
- Better handle array spin-up/downs to save power (it really adds up in SoCal)
- Improve the viewing experience for myself
- No other tool did what I wanted — straightforward, simple setup and teardown
This started life as plex-hot-cache — a Plex-specific caching layer. As the design matured, I realized that the FUSE and caching logic was the main "magic" here, so it became fscache and open to all services. The Plex-specific smarts (episode prediction, transcoder filtering) are still here as a preset, but the tool works just as well for any read-heavy generic file workload.
I plan on supporting this software to flesh out even more features (write caching, more eviction algos, more caching algos, other software integrations, etc.)
![]() |
![]() |
This is a new project. I HIGHLY recommend you DISABLE automatic trash emptying in Plex while evaluating this software. Filesystem mounting/unmounting is potentially dangerous on a live server. If Plex detects a drive went down and you have automatic trash cleanup enabled, it WILL delete your Plex metadata (not the files — just watch history, ratings, etc.). My codebase has extensive automated testing that protects against this type of failure, but please be safe. If you're using this tool, you're probably hoarding data like me and I would HATE to see a critical bug break your metadata.
fscache mounts a read-only FUSE filesystem directly over the existing filesystem directory. Your application keeps reading from the same path it always has. When a file is opened, FUSE intercepts the request and serves it from the SSD cache if available, falling back to the original backing store transparently if not.
In the background, a preset-driven action engine watches which files are being opened and decides what else to cache — the next N episodes, neighboring files, or an entire directory tree, depending on how you configure it.
Application (Plex, Jellyfin, etc.)
│
▼
/mnt/media ← FUSE overmount (fscache)
│
├─ cache hit → /mnt/ssd-cache/... (fast, local SSD)
└─ cache miss → /mnt/nas/media/... (slow, network share / drive array)
On shutdown, the FUSE mount is lazily detached — any streams already in progress continue uninterrupted from their open file descriptors.
Behavior is controlled by a preset — a pluggable strategy for deciding what to cache and when.
Episode lookahead tailored for Plex. Pre-caches the next N episodes after any file open. Intelligently filters out Plex's background analysis processes (Media Scanner, EAE Service, Fingerprinter, intro/credit detection transcoders) so only real user playback drives the prediction. Supports two modes:
miss-only(default) — predict and cache ahead only on a cache missrolling-buffer— keep the next N episodes loaded at all times, re-predicting on every access
The episode-prediction name is accepted as an alias.
Configured via the [plex] section.
General-purpose preset with three caching modes and optional regex filtering. Not Plex-specific — works for any file workload.
Modes:
cache-hit-only— cache only the file that was accessed. Safest, no fan-out.cache-neighbors— cache all sibling files in the same directory. Good for media folders where related files (episodes, artwork, metadata) live side by side.cache-parent-recursively— recursively cache the entire parent directory up tomax_depthlevels deep. Aggressive, but useful when a single access should warm an entire show or album.
Filtering:
file_whitelist— regex patterns matched against filename. Only matching files are cached. Empty list means allow all.file_blacklist— regex patterns matched against filename. Matching files are never cached. Blacklist is checked first and always wins over whitelist.process_blocklist— process names (and their children) that must never trigger caching.
All regex patterns are compiled at startup. Invalid patterns cause the daemon to refuse to start — no silent failures at runtime.
Configured via the [prefetch] section.
Set the preset via [preset] name in config.toml.
- Launch at any time. The FUSE mount can go up or come down without restarting Plex. Streams already in flight are not interrupted on shutdown.
- Graceful by default. Cache corruption, copy failures, and missing files are all handled without crashing — the worst case is a cache miss that falls back to the network share.
- Drop-in / drop-out. No modifications to Plex or your media library. Remove the service and your media directory is exactly as it was.
- Multiple mounts. Point fscache at multiple media directories simultaneously — each gets its own namespaced cache subdirectory.
Grab the latest release from the Releases page and extract it wherever you want to run it from:
tar -xzf fscache-*.tar.gz -C /opt/fscacheThe release includes the binary, a default config.toml, and the LICENSE.
Edit config.toml in the same directory as the binary. At minimum, set three values:
[paths]
target_directories = ["/mnt/media"] # directories to overmount (can list multiple)
cache_directory = "/mnt/ssd-cache" # SSD path for cached files
instance_name = "my-media" # unique name for this instance (used for DB and process lock)See the full Settings section below for all options.
Plex runs as a different user, so it needs permission to access the FUSE mount:
echo "user_allow_other" | sudo tee -a /etc/fuse.confcd /opt/fscache
sudo ./fscacheThat's it. The cache is active and filesystem doesn't need any changes. Stop it with Ctrl+C — the mount detaches cleanly. LET IT DO IT'S THING, DO NOT DOUBLE TAP CTRL+C.
If you want to monitor it with the GUI, simply run ./fscache watch.
For a persistent setup that starts on boot:
sudo cp fscache.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now fscacheVerify it's running:
systemctl status fscache
mount | grep fscacheThe service file is included in the release. If your media is on a network share, edit the unit to wait for the mount — see the comments inside fscache.service.
cargo build --release
sudo cp target/release/fscache /usr/local/bin/| Setting | Description |
|---|---|
paths.target_directories |
List of media directories Plex reads from (each will be FUSE overmounted) |
paths.cache_directory |
SSD path where cached files are stored |
paths.instance_name |
Unique name for this instance — used for DB filename and process lock |
| Setting | Default | Description |
|---|---|---|
preset.name |
plex-episode-prediction |
plex-episode-prediction or prefetch |
Used when preset.name is plex-episode-prediction (or episode-prediction).
| Setting | Default | Description |
|---|---|---|
plex.lookahead |
4 |
Episodes to pre-cache ahead of current position |
plex.mode |
miss-only |
miss-only (predict on miss) or rolling-buffer (keep next N always loaded) |
plex.process_blocklist |
[] |
Process names that must never trigger prediction |
Used when preset.name is prefetch.
| Setting | Default | Description |
|---|---|---|
prefetch.mode |
cache-hit-only |
cache-hit-only, cache-neighbors, or cache-parent-recursively |
prefetch.max_depth |
3 |
Max recursion depth for cache-parent-recursively mode |
prefetch.process_blocklist |
[] |
Process names that must never trigger caching |
prefetch.file_whitelist |
[] |
Regex patterns — only matching filenames are cached (empty = allow all) |
prefetch.file_blacklist |
[] |
Regex patterns — matching filenames are never cached (checked before whitelist) |
| Setting | Default | Description |
|---|---|---|
eviction.max_size_gb |
200.0 |
Max total SSD cache size across all mounts |
eviction.expiry_hours |
72 |
Remove cached files not accessed within this window |
eviction.min_free_space_gb |
10.0 |
Stop caching if SSD free space drops below this |
| Setting | Default | Description |
|---|---|---|
cache.passthrough_mode |
false |
Bypass cache entirely — useful for debugging |
cache.max_cache_pull_per_mount_gb |
0.0 (unlimited) |
Cap per-mount lookahead pull per session |
cache.deferred_ttl_minutes |
1440 |
Discard buffered events (from outside caching window) older than this on startup |
cache.min_access_secs |
0 |
Minimum seconds a file must stay open before prediction triggers |
cache.min_file_size_mb |
0 |
Skip files below this size in MB — filters metadata/subtitle noise |
| Setting | Default | Description |
|---|---|---|
schedule.cache_window_start |
08:00 |
Start of allowed caching window (HH:MM, 24h) |
schedule.cache_window_end |
02:00 |
End of allowed caching window (wraps past midnight) |
Accesses outside the window are buffered and flushed when the window re-opens.
| Setting | Default | Description |
|---|---|---|
logging.log_directory |
/var/log/fscache |
Directory for rolling daily log files |
logging.console_level |
info |
Terminal log level (error/warn/info/debug/trace) |
logging.file_level |
debug |
Log file level |
logging.repeat_log_window_secs |
300 |
Suppress repeated access logs for the same file within this window |
[paths]
target_directories = ["/mnt/media", "/mnt/media2"]
cache_directory = "/mnt/ssd-cache"
instance_name = "plex-movies"
[preset]
name = "plex-episode-prediction"
[plex]
lookahead = 4
mode = "miss-only"
process_blocklist = ["Plex Media Scanner", "Plex EAE Service", "Plex Media Fingerprinter"]
[eviction]
max_size_gb = 200.0
expiry_hours = 72
min_free_space_gb = 10.0
[cache]
passthrough_mode = false
max_cache_pull_per_mount_gb = 0.0
min_access_secs = 0
min_file_size_mb = 0
[schedule]
cache_window_start = "08:00"
cache_window_end = "02:00"
[logging]
log_directory = "/var/log/fscache"
console_level = "info"
file_level = "debug"
repeat_log_window_secs = 300[preset]
name = "prefetch"
[prefetch]
mode = "cache-neighbors"
max_depth = 3
file_blacklist = ["\\.nfo$", "\\.jpg$"]
# file_whitelist = ["\\.mkv$", "\\.mp4$"]
