Back to blog
FILE 0x72·QBITTORRENT'S API RATE-LIMITS FRESH LOGINS

qBittorrent's API rate-limits fresh logins

April 28, 2026 · homelab, qbittorrent, api

A dashboard tile of mine that aggregates torrent counts across five qBittorrent instances started intermittently returning 403s. The page would refresh and most numbers would be there, but two or three of the five clients would be missing every other load.

What was happening

The tile was calling POST /api/v2/auth/login against each client, then GET /api/v2/torrents/info to pull counts, then forgetting the session and doing the same dance on the next refresh. Page refreshes every 30 seconds across five clients meant roughly ten logins per minute.

qBittorrent's WebUI considers that a brute-force pattern. After a few minutes of that, it starts rejecting logins with a 403 and a banner about rate limiting. The 403s were intermittent because the rate-limit window was per-client.

What I found

The fix is dumb-obvious in retrospect: stop logging in on every request. The WebUI hands you a SID cookie that's valid until you log out. Reusing it across requests is the supported pattern, and qBittorrent treats authenticated requests under an existing SID as completely separate from login attempts for rate-limit purposes.

The fix

Cache the SID per client, only re-authenticate on a 403 response:

import requests
from time import time

SESSIONS = {}  # client_id -> (sid, expires_at)

def qbt_get(client, path):
    sid, expires = SESSIONS.get(client.id, (None, 0))
    if not sid or expires < time():
        sid = qbt_login(client)
        SESSIONS[client.id] = (sid, time() + 300)  # 5-min TTL

    r = requests.get(
        f"{client.base_url}{path}",
        cookies={"SID": sid},
        timeout=5,
    )
    if r.status_code == 403:
        sid = qbt_login(client)
        SESSIONS[client.id] = (sid, time() + 300)
        r = requests.get(
            f"{client.base_url}{path}",
            cookies={"SID": sid},
            timeout=5,
        )
    r.raise_for_status()
    return r.json()

Five-minute TTL is conservative — qBittorrent's session timeout default is 60 minutes, but I'd rather re-auth occasionally than be holding a stale SID for an hour. The 403 fallback handles the case where the session expired before the TTL was up (e.g., if qBittorrent was restarted in the middle).

While I was in there, I also moved the per-client fetches into a Promise.all-style parallel call with a 5-second timeout per request, instead of doing them sequentially. Page load dropped from ~3s to under 1s.

What I'd do differently

Caching sessions is a basic thing to think about and I had to get bitten before I added it. Any external API where the auth step is more expensive than the data fetch deserves a session cache from day one. The pattern is also a free hint that there's an authenticated long-lived state worth reusing — if a vendor only gives you ephemeral request-scoped auth, you can't cache it, and the rate-limit pressure becomes the vendor's problem to solve. With qBittorrent, the session cookie is right there in the response headers; ignore it at your peril.