11 KiB
Transcodes
Ramdisk-based transcode storage management for Emby using filesystem symlink indirection.
The Problem
Emby transcodes video on the fly for clients that can't play the source format directly. Each transcode session generates hundreds of small HLS segment files that are written and read continuously. Where those files live has a significant impact on performance:
- Hard drives — too slow for simultaneous multi-stream transcoding. Seek times cause buffering.
- SSD (cache pool) — fast enough, but constant small file writes accelerate wear over time.
- RAM (tmpfs) — fastest possible, no wear, disappears cleanly when sessions end.
A ramdisk is the ideal transcode location. The only risk is running out of RAM during heavy load — which is where this system comes in.
The Design
Symlink Indirection
Emby is pointed at a fixed path that never changes:
/mnt/ram-transcode → [currently: /mnt/ramdisk_transcodes]
This is a symlink. Emby doesn't know or care what's on the other end — it just writes to /mnt/ram-transcode. The transcode manager controls where that path actually points by updating the symlink target.
The critical insight: ffmpeg resolves the symlink path once at session start. After that, it has a direct reference to the actual directory. This means:
- Existing sessions are never affected by symlink changes
- When the symlink flips from ramdisk to SSD, sessions already in progress keep writing to ramdisk until they end naturally
- Only new sessions care about where the symlink currently points
This is what makes the fallback seamless. Users never experience a glitch.
Why Not Just Use the SSD Directly?
You could point Emby directly at the SSD and skip the ramdisk entirely. Many setups do this. The ramdisk approach gives you:
- Faster performance — RAM is orders of magnitude faster than SSD for small random writes
- Zero SSD wear — transcode segments are written and deleted constantly. On a busy server this adds up to significant SSD wear over months and years
- Automatic cleanup — tmpfs is released back to the system when files are deleted. No fragmentation, no stale files surviving a crash
- Session isolation — each session's files disappear completely when the session ends
The Scripts
ramdisk_setup.sh
Run at array start. Run once.
Creates the tmpfs ramdisk, the SSD fallback directory, and the symlink. If the ramdisk is already mounted it reports status and exits cleanly — safe to run multiple times.
# Scheduled as: At Startup of Array
/mnt/user/appdata/unraid_scripts/Transcodes/ramdisk_setup.sh
What it creates:
/mnt/ramdisk_transcodes/ ← tmpfs mount (RAMDISK_SIZE ceiling)
/mnt/ram-transcode ← symlink pointing at ramdisk
/mnt/cache/Temp_Storage/Emby/Transcodes/ ← SSD fallback directory
After running, verify:
mountpoint /mnt/ramdisk_transcodes # should say "is a mountpoint"
readlink /mnt/ram-transcode # should show /mnt/ramdisk_transcodes
transcode_manager.sh
Run every 3 minutes via cron.
Monitors ramdisk usage and manages the symlink direction. The main brain of the system.
# Scheduled as: */3 * * * *
/mnt/user/appdata/unraid_scripts/Transcodes/transcode_manager.sh
Operating Modes
Set TRANSCODE_MANAGER_MODE in Master.conf:
| Mode | Behavior | Use When |
|---|---|---|
smart |
Auto-flips between ramdisk and SSD based on thresholds | Normal operation — default |
ramdisk |
Always uses ramdisk, never flips to SSD | Light load, guaranteed RAM performance |
ssd |
Always uses SSD, never uses ramdisk | Ramdisk maintenance, post-flip drain |
Smart Mode — How the Flip Works
Ramdisk usage rises above RAMDISK_WARN_GB (6.8GB)
→ Symlink flips to SSD
→ New sessions land on SSD
→ Existing sessions continue on ramdisk until they end
Ramdisk usage drops below RAMDISK_LOW_GB (5.5GB)
→ Symlink flips back to ramdisk
→ New sessions land on ramdisk again
The gap between RAMDISK_WARN_GB and RAMDISK_LOW_GB (1.3GB) is the hysteresis gap. It prevents the symlink from flip-flopping when usage hovers near the threshold. Without this gap you'd get constant flipping on a busy system.
Safety Checks
Every run, regardless of mode:
| Condition | Action |
|---|---|
| Ramdisk not mounted | Flip symlink to SSD immediately, notify warning |
| SSD path missing | Disable fallback, notify warning. If mode is ssd — exit |
| Symlink missing | Recreate pointing at ramdisk, notify |
| Symlink target gone | Reset to ramdisk, notify |
| Permissions drift | Fix silently — chmod and chown applied every run |
| Emby not running | Skip threshold checks, verify symlink only |
Session Display
Each run queries the Emby API and shows active streams:
━━━ 🎬 Active Emby Sessions ━━━
🎬 Total: 7 | 💨 Live TV: 5 | 🔄 Transcoding: 5 | 🏁 Direct: 2
🔗 Storage: 💨 ramdisk
🎬 Gmer4Lfe — MLB: Pirates vs Nationals — Live TV — Transcode
🎬 Rebecca — MLB: Pirates vs Nationals — Live TV — Transcode
🎬 Sunny — AT&T Sportsnet Pittsburgh — Live TV — Transcode
🎬 jaden — TNT — Live TV — Transcode
🎬 Mama Bear — Con-Text — TV Show — Direct Stream
Split state is detected and displayed when sessions exist on both ramdisk and SSD simultaneously — this happens naturally when the symlink flips while sessions are in progress:
⚠️ Split state — 4 folder(s) on ramdisk / 2 on SSD
⚠️ Older sessions remain on original location until they end naturally
🔗 Storage: 💨 ramdisk (4) + 💾 SSD (2)
Why per-session location isn't shown: Emby's internal transcode folder names don't match the session IDs returned by the API — there is no reliable way to map a specific user to a specific folder. The folder count on each location gives you the picture you need at a glance without false precision.
Flip Frequency Warning
If the symlink flips TRANSCODE_FLIP_WARN or more times in one hour, a notification is sent. This is a signal that RAMDISK_SIZE may need to be increased. Real production data from this setup:
Normal load (2-3 streams) → ~1.5-2.0GB
Busy evening (5-6 streams) → ~3.5-4.5GB
Peak (8 streams, live TV) → ~5.2GB
Threshold trigger → 6.8GB
transcode_cleanup.sh
Run every 5 minutes via cron.
Removes old inactive transcode files from both ramdisk and SSD. Never deletes files that are currently open by any process.
# Scheduled as: */5 * * * *
/mnt/user/appdata/unraid_scripts/Transcodes/transcode_cleanup.sh
Deletion Rules
A file is eligible for deletion only when all of these are true:
- Older than
TRANSCODE_MAX_AGEminutes (default: 20 min) - Not currently open by any process
Performance Design
lsof is called once per location to build a complete list of open files — not once per file. This is critical on a busy Live TV system where a single location can have thousands of HLS segment files. A per-file lsof approach stalls the system under load.
Docker Mount — Critical
Emby must be configured with one transcode mount only:
Host path: /mnt/ram-transcode/
Container path: /ext-ram-transcode
Do NOT add a static SSD transcode path as a second volume mount.
If the SSD path is mounted inside the container, Emby can see it as an accessible transcode location and will route sessions there independently of the symlink — completely bypassing the management system. This is not obvious and causes confusing split behavior that is hard to diagnose.
The symlink handles all routing. One mount is all that's needed.
This was learned in production. The system worked correctly once the static SSD mount was removed. The symptom was new sessions landing on SSD even when the symlink pointed at ramdisk.
Emergency Manual Flip
If you need to manually redirect all new transcodes to SSD:
ln -sfn /mnt/cache/Temp_Storage/Emby/Transcodes /mnt/ram-transcode
To flip back to ramdisk:
ln -sfn /mnt/ramdisk_transcodes /mnt/ram-transcode
Existing sessions are unaffected — only new sessions follow the new target.
Configuration
All configuration in Master.conf under the ── TRANSCODES ── section.
# Paths
RAMDISK_PATH="/mnt/ramdisk_transcodes" # tmpfs mount point
RAMDISK_SIZE="8G" # ceiling — only uses RAM actually needed
TRANSCODE_LINK="/mnt/ram-transcode" # symlink — location never changes
TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/"
# Smart mode thresholds
RAMDISK_WARN_GB=6.8 # flip to SSD above this
RAMDISK_LOW_GB=5.5 # flip back to ramdisk below this
RAMDISK_SSD_MIN_GB=20 # minimum SSD free space before allowing flip
# Cleanup
TRANSCODE_MAX_AGE=20 # minutes before file eligible for cleanup
TRANSCODE_ORPHAN_AGE=30 # minutes for orphaned files
# Alerts
TRANSCODE_FLIP_WARN=3 # notify if symlink flips this many times per hour
# Permissions
TRANSCODE_OWNER="nobody:users"
TRANSCODE_CHMOD="755"
# Mode
TRANSCODE_MANAGER_MODE="smart" # smart | ramdisk | ssd
# Emby check
TRANSCODE_CHECK_EMBY=true
TRANSCODE_EMBY_CONTAINER="Emby"
Sizing the Ramdisk
The ramdisk is a tmpfs — it only uses RAM that is actually needed. RAMDISK_SIZE is a ceiling, not a reservation. An 8GB ramdisk that holds 2GB of files only uses 2GB of RAM.
Rule of thumb for sizing:
- Count your maximum expected concurrent transcoding streams
- Multiply by ~0.5-1GB per stream (Live TV HLS streams use more than standard transcodes)
- Add 20-30% headroom above your threshold
From production data on this setup:
- 5 Live TV streams + 2 standard = ~4.5GB
- 8 streams peak = ~5.2GB
- Current ramdisk = 8GB with 6.8GB threshold — comfortable headroom
If you regularly hit TRANSCODE_FLIP_WARN or see 3+ flips per hour, increase RAMDISK_SIZE by 2GB and adjust thresholds accordingly.
Sizing Thresholds
When adjusting RAMDISK_SIZE, adjust thresholds to match:
| Ramdisk Size | RAMDISK_WARN_GB | RAMDISK_LOW_GB |
|---|---|---|
| 6G | 4.8 | 3.5 |
| 8G | 6.8 | 5.5 |
| 10G | 8.5 | 7.0 |
| 12G | 10.0 | 8.5 |
Keep a 1.0-1.5GB hysteresis gap between WARN and LOW. A gap smaller than this causes flip-flop behavior near the threshold.
Scheduled Run Summary
| Script | Schedule | Purpose |
|---|---|---|
ramdisk_setup.sh |
At Startup of Array | Create ramdisk and symlink |
transcode_manager.sh |
*/3 * * * * |
Monitor usage, manage symlink, display sessions |
transcode_cleanup.sh |
*/5 * * * * |
Remove old inactive files |
Version 2 Roadmap
A future advanced mode is planned that allows per-media-type storage routing:
TRANSCODE_MANAGER_MODE="advanced"
TRANSCODE_FORCE_RAMDISK=(
"LiveTv" # always ramdisk — buffering is latency sensitive
)
TRANSCODE_FORCE_SSD=(
"Audio" # music downloads — no benefit from ramdisk
)
# Everything else follows smart threshold behavior
This requires the Emby API to expose media type at session start — the groundwork (session display and media type parsing) is already in place. Target: this fall.