qBittorrent Incomplete Downloads on NVMe: The Fast, Practical Proxmox Setup Guide

qbittorrent-incomplete-downloads-on-nvme.md
title qBittorrent Incomplete Downloads on NVMe: The Fast, Practical Proxmox Setup Guide
date
author VahaC
read 8 min read
category Self-Hosting, Uncategorized
tags #Docker #Homelab #Jellyfin #lxc #Proxmox #qBittorrent #Ssd #storage #Telegram
qBittorrent incomplete downloads on NVMe

Keeping qBittorrent incomplete downloads on NVMe is the simplest way to stop torrents from hammering the hard drive that holds your media library. Torrents write in small random pieces, which is exactly the workload an HDD hates. With one built-in qBittorrent option, every torrent downloads to a fast SSD first and lands on the big disk only when it is complete.

This post continues my guide to migrating Proxmox LXC to NVMe. After that move, my 4 TB NVMe pool had plenty of free space, so I used a slice of it for qBittorrent incomplete downloads on NVMe. 🚀

Why Keep qBittorrent Incomplete Downloads on NVMe? 🧠

A torrent client receives pieces in random order from dozens of peers. On an HDD, every piece means a head seek. With a few active torrents, the disk spends its time seeking instead of streaming to Jellyfin. Here is what changes when you put qBittorrent incomplete downloads on NVMe:

  • Random writes go to the SSD. The HDD only gets one sequential write per torrent, when qBittorrent moves the finished files.
  • No half-finished files in your library. Jellyfin, Radarr and Sonarr only see files after they are complete.
  • Seeding keeps working. qBittorrent seeds from the final folder on the HDD, so nothing changes for your ratio.

In an older post I moved finished downloads to another storage location with a separate script. With qBittorrent 5.x you get qBittorrent incomplete downloads on NVMe natively through Keep incomplete torrents in: no extra script, no cron job.

Prerequisites ✅

Here is what you need for qBittorrent incomplete downloads on NVMe:

  • A Proxmox VE host with root shell access (tested on Proxmox VE 9.2)
  • An NVMe-backed LVM-thin storage in Proxmox. Mine is called nvme-data; see how to add LVM-thin storage in Proxmox
  • An LXC container that runs qBittorrent in Docker. I use the LinuxServer.io qBittorrent image (qBittorrent 5.2)
  • Your media HDD already bind-mounted into that LXC, for example at /mnt/media

In the examples, the container ID is 105. Replace it with your own.

Step 1 — Create a Volume for qBittorrent Incomplete Downloads on NVMe

Do not put the downloads on the container’s root disk. It is usually small, and your backups would copy half-finished torrents every night. Instead, give qBittorrent incomplete downloads on NVMe their own mount point volume. Run this on the Proxmox host:

pct set 105 -mp1 nvme-data:500,mp=/mnt/qbit-incomplete,backup=0
  • mp1 is the next free mount point. If the media disk is already mp0, use mp1; otherwise pick the next free number.
  • nvme-data:500 creates a new 500 GB thin volume on that storage.
  • backup=0 leaves the volume out of vzdump and Proxmox Backup Server jobs. Unfinished torrents can be downloaded again.

Prefer the web UI? Open the container, go to Resources → Add → Mount Point, choose the NVMe storage, set the size and path, and untick Backup. My post on editing Proxmox LXC config in the web UI walks through that screen. All options are in the pct manual.

⚠️ Critical: size the volume, do not overcommit the pool. A thin volume never grows past its own size. If downloads fill it, qBittorrent stops those torrents with an I/O error and everything else keeps running. But if the sizes of all volumes add up to more than the pool and the pool itself runs full, every container on it loses write access at once. Pick a size bigger than everything you download at the same time, and keep the total under the pool size.

Check that the volume is mounted inside the container:

pct exec 105 -- df -h /mnt/qbit-incomplete

If the web UI shows the new mount point in orange (pending), restart the container once.

Step 2 — Give qBittorrent Write Access

A new volume belongs to root. The LinuxServer.io image runs qBittorrent as the user set in PUID and PGID, so hand the folder to that user:

pct exec 105 -- chown 1000:100 /mnt/qbit-incomplete

Use the same numbers as in your compose file. Without this step, qBittorrent incomplete downloads on NVMe fail with a permission error.

Step 3 — Mount the Volume into the qBittorrent Container 🐳

Add one line to the volumes section of the qBittorrent service in docker-compose.yml:

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    environment:
      - PUID=1000
      - PGID=100
    volumes:
      - /mnt/media/qbittorrent/config:/config
      - /mnt/media/Torrent:/downloads
      - /mnt/qbit-incomplete:/incomplete   # new: incomplete torrents on NVMe

Recreate only this container:

docker compose up -d qbittorrent

Only qBittorrent needs the new mount. Radarr, Sonarr and Jellyfin keep their existing /downloads or media mounts, because they only ever touch finished files.

⚠️ Do not change the /downloads mapping. Radarr and Sonarr find files through the paths qBittorrent reports. If the final path changes, imports break.

Step 4 — Enable qBittorrent Incomplete Downloads on NVMe in the WebUI ⚙️

Open the qBittorrent WebUI and go to Tools → Options → Downloads → Saving Management:

  1. Set Default Torrent Management Mode to Automatic, so categories decide the final folder.
  2. Tick Keep incomplete torrents in and enter /incomplete/.
  3. Click Save.

Categories inherit the setting when their incomplete-torrents path is left at Default. qBittorrent creates a subfolder per category, for example /incomplete/Movies, and moves the files to the category’s save path when the torrent completes. For a torrent added in manual mode, tick Use another path for incomplete torrent in the add dialog.

⚠️ Do not edit qBittorrent.conf while the container runs. qBittorrent writes its settings back on exit and overwrites your changes. Use the WebUI.

Torrents still downloading in Automatic mode move to the NVMe as soon as you save. Finished torrents stay put.

Step 5 — Verify qBittorrent Incomplete Downloads on NVMe ✔️

Add a small legal torrent, such as Sintel from the WebTorrent free torrents page. While it downloads, the files should appear on the NVMe volume:

pct exec 105 -- ls -la /mnt/qbit-incomplete

When it finishes, the qBittorrent log (View → Log, or /config/qBittorrent/logs/qbittorrent.log) shows lines like these:

Enqueued torrent move. Torrent: "Sintel". Source: "/incomplete/Cartoons". Destination: "/downloads/cartoons"
Moved torrent successfully. Torrent: "Sintel". Destination: "/downloads/cartoons"
Torrent download finished. Torrent: "Sintel"

The files are now in the category folder on the HDD, and the torrent is seeding from there. That proves qBittorrent incomplete downloads on NVMe work end to end. Delete the test torrent together with its files.

Bonus: Fix Telegram Notifications That Silently Fail 📲

If you followed my Telegram notifications guide for qBittorrent, good news first: the script still works with qBittorrent incomplete downloads on NVMe. qBittorrent runs it after the move, so %D already holds the final path.

While testing, I found a bug in that script. It sends messages in Telegram’s Markdown mode, where _ starts italic text. A torrent name like Some_Movie_2019_1080p.mkv leaves an unclosed tag, Telegram answers Bad Request: can't parse entities, and curl -s hides the error. No message arrives.

The fix is HTML formatting mode: only &, < and > need escaping there. This version also logs every run, so the next failure is easy to diagnose:

#!/bin/bash
# qBittorrent: Run on torrent finished
# /scripts/qbt_notify_telegram.sh "%N" "%D" "%L"

BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"

LOG=${QBT_NOTIFY_LOG:-/scripts/qbt_notify.log}
exec >>"$LOG" 2>&1
echo "$(date '+%F %T') start args=[$1] [$2] [$3]"

# HTML mode: only & < > need escaping, so _ * [ ] in names are safe
esc() {
    local s=$1
    s=${s//"&"/"&amp;"}
    s=${s//"<"/"&lt;"}
    s=${s//">"/"&gt;"}
    printf "%s" "$s"
}

MESSAGE="✅ Torrent finished: <b>$(esc "$1")</b>
📂 Path: <code>$(esc "$2")</code>
🏷 Category: <b>$(esc "${3:-—}")</b>"

# --data-urlencode keeps & + # in names from breaking the request
curl -sS --max-time 20 --retry 3 -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
    --data-urlencode "chat_id=$CHAT_ID" \
    --data-urlencode "text=$MESSAGE" \
    --data-urlencode "parse_mode=HTML"

echo " curl_exit=$?"

The LinuxServer.io image already ships bash and curl. After each finished torrent, qbt_notify.log shows the arguments, Telegram’s answer (look for "ok":true) and the curl exit code.

Troubleshooting 🛠️

Common problems with qBittorrent incomplete downloads on NVMe:

  • Torrent shows “Moving” for minutes: normal for big releases; moving between two disks is a real copy.
  • Leftover files on the NVMe: if you delete a torrent while it is being moved, a copy can stay behind. Compare /mnt/qbit-incomplete with your torrent list now and then.
  • Torrents show “Errored” or “Permission denied”: the folder owner does not match PUID/PGID. Repeat Step 2.
  • Volume full: grow it from the host with pct resize 105 mp1 +200G. Check pool space with lvs first.
  • Nothing lands on the NVMe: the torrent is in manual mode, or its category has No instead of Default for the incomplete path.

Final Thoughts

Moving qBittorrent incomplete downloads on NVMe took four small changes: a volume, a chown, a compose line and a checkbox. The HDD now gets a single sequential write per torrent instead of thousands of random ones, and Jellyfin never sees a half-downloaded file.

Worried about SSD wear? Each downloaded byte is written to the NVMe only once, and a 4 TB consumer drive is rated for hundreds of terabytes written. The volume does not need redundancy either: the worst you can lose with qBittorrent incomplete downloads on NVMe is a torrent you can simply download again. 🏠

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.