Skip to content

Hit-and-Run (HnR) ​

A hit-and-run is a user who downloads a torrent and stops seeding before the operator-defined seed-time threshold. Trackarr enforces HnR at the per-(user, torrent) level so the consequences land on the responsible account, not the operator's mood.

Moderation · Hit & Run — flagged rows with seed time against the requirement, and the per-row clear / exempt actions

Concepts ​

TermMeaning
Required seed timeHow long a user must seed a given torrent before HnR is cleared. Default 24 h. Set in hours from /admin/settings → Hit & Run (stored in seconds as hnr_required_seed_time). The torrent page announces it before the download ("commits you to N h of seeding").
Grace periodWindow after downloadedAt during which a not-yet-completed download is not flagged. Default 72 h, set in hours from /admin/settings → Hit & Run (hnr_grace_period). Tracking itself is switched on there too (hnr_enabled, off by default).
CompletedA row reached seedTime >= requiredSeedTime. completedAt is set; isHnr is forced to false and never re-flips.
ExemptModerator manually waives the requirement. Engine ignores the row from that point onward.
Flagged (isHnr = true)The grace period expired, the user still hadn't met the requirement. Auto-clearable by completing or by moderator action.

Data model — hnr_tracking ​

One row per (user, torrent). The unique index hnr_user_torrent_idx guarantees idempotency.

ColumnNotes
userIdFK → users (cascade delete).
torrentIdFK → torrents (cascade delete).
downloadedAtWhen the user first either clicked Download or completed the torrent.
seedTimeAccumulated seconds seeded, credited by the tracker on each seeding announce (see below).
requiredSeedTimeSnapshot of the threshold at insert time. Lets the operator change the global default without retroactively re-graded existing rows.
isHnrtrue once the grace window expired without completion.
isExemptManual moderator waiver.
completedAtSet when seedTime >= requiredSeedTime. Final state for that row.
uploaded, downloadedPer-(user, torrent) byte counters maintained by the tracker. Drives the /downloads history page.

Lifecycle ​

                     POST /api/torrents/:hash/download
                                  │
                                  ▼
                          recordDownloadClick()
                          (ON CONFLICT DO NOTHING)
                                  │
                                  ▼
                       ┌─────────────────────────┐
                       │ row inserted            │
                       │  isHnr     = false      │
                       │  completedAt = null     │
                       └────────────┬────────────┘
                                    │
                ┌───────────────────┼───────────────────┐
                │                   │                   │
                ▼                   ▼                   ▼
   tracker announce         grace expired,       moderator action
   updates seedTime         not yet completed     `exempt` / `clear`
                │                   │                   │
                ▼                   ▼                   ▼
        seedTime ≥ required?   checkAndMarkHnrs    isExempt = true
                │              flips isHnr=true    OR isHnr=false
                ▼              + notify             + completedAt=now
        completedAt = now
        isHnr = false (final)

Where the seed-time delta comes from ​

The Go tracker's announce handler diffs uploaded / downloaded against the previous peer entry on every announce, adds the increments to hnr_tracking, and books them on the member's totals (batched through Redis — see Scaling).

Seed time is credited on the same path, by an announce that reports the torrent complete (left=0; a BEP 21 partial seed does not count). It credits the stretch since the account was last credited on that torrent — or since this peer's previous announce, if that is later — and at most two announce intervals, an hour, at once. How far each account has been credited is a mark in Redis (seedtime:<torrent>:<user>), so the stretches never overlap:

  • a member seeding from three clients earns real time, not three times it;
  • a client that re-announces every 30 minutes earns all 30 (until October 2026 it earned 15: each credit was capped at the minimum interval, which doubled every requirement);
  • a client silent for longer than an hour is credited the hour, not the gap.

Exempt or completed rows are left alone: the update matches only is_exempt = false AND completed_at IS NULL.

The previous-announce snapshot lives in Redis with a 24 h TTL by default, configurable via TRACKER_PEER_TTL (Go duration string — 24h, 90m, 7200s; values below 15m are clamped to that floor). Two cases credit nothing:

  • First announce ever for a given (peer_id, info_hash) pair — Redis has no prev, so there is nothing to diff against.
  • Gap longer than the TTL — same effect, the peer hash expired between announces.

Both only re-establish the baseline; the next announce credits normally. The tracker never takes a client's declared cumulative counters as a delta: a client looping started(uploaded=1TiB) → stopped → started would otherwise mint credit without end. A restart forfeits at most one announce interval.

When rows get flagged ​

checkAndMarkHnrs() runs every ten minutes from apps/api/plugins/hnr-sweep.ts (first pass two minutes after boot), under a cross-replica lock so each member is notified once. Opening the moderation queue with ?check=true still runs it on demand. Until October 2026 that was the only way it ran — on an instance nobody moderated by hand, nobody was ever flagged. On each pass:

  1. It reads the current grace period from /admin/settings (getHnrGracePeriod).
  2. It selects every row where isHnr = false, isExempt = false, completedAt IS NULL, downloadedAt < now - graceWindow, and downloaded > 0 — the row is created when the member clicks Download, and one who never started the torrent took nothing from the swarm.
  3. It flips those rows to isHnr = true in a single UPDATE … RETURNING.
  4. For each flipped row it fans out a hnr_violation_marked notification to the user (resolving the torrent name so the inbox row is meaningful).

This means the user is always notified the moment they cross the line — they don't have to discover the flag by checking their profile.

What gets flagged user-visibly ​

SurfaceNotes
/downloadsPersonal history of every torrent the user clicked Download on. HnR rows are colour-rail flagged with a countdown ("⏳ 4h 12m remaining" before grace expiry) or the active flag once expired.
/meHero KPI strip carries the user's current HnR count.
/users/[id]Public profile shows the same KPI to other members.
/mod/hnrModerator queue (filterable by status: open / completed / exempt).
Notificationhnr_violation_marked → goes through the user's notification routing.

Moderator actions ​

The queue at /mod/hnr exposes two actions per row:

ActionEffect
ExemptSets isExempt = true. Future seed-time updates skip the row; the flag never re-fires.
ClearSets isHnr = false and completedAt = now, the same final state a legitimate completion reaches. Use this for one-off forgiveness.

Both end up in hnr_user_is_hnr_idx-indexed queries the same way — the audit columns (completedAt, isExempt) tell the two paths apart.

Auto-role interactions ​

roleRules.ts exposes hnrCount (current isHnr = true rows) and completedSeeds (rows with completedAt IS NOT NULL) as auto-assignment fields. Common patterns:

  • "Member" → "Power User" when completedSeeds ≥ 50 AND hnrCount = 0.
  • "Member" → "Probation" when hnrCount ≥ 3.

When a moderator exempts or clears a flag, the affected user's roles are not auto-recomputed by the action itself — admins can trigger a sweep from /admin/roles (POST /api/admin/roles/recompute).

See Roles & Permissions.

Disabling HnR ​

Set the feature flag to off in /admin/settings:

  • New downloads stop inserting hnr_tracking rows via createHnrEntry().
  • Existing rows are kept, but checkAndMarkHnrs() becomes a no-op (returns 0 immediately).
  • The recordDownloadClick() path still records a row, because the Downloads page is a personal log — it shouldn't vanish when the operator switches enforcement off.

Bonus interactions ​

The seed-bonus accumulator reads seedTime from the same table to credit seeding_milestone grants (24 h / 1 week / 30 d thresholds). HnR enforcement and bonus crediting share their seedTime source-of-truth — there's no risk of "rewarded for seeding but still flagged" inconsistencies.

See Seed Bonus.

API surface ​

MethodPathAuthNotes
GET/api/users/hnruserCurrent user's flagged rows (used by the bell badge fallback).
GET/api/admin/hnrmodQueue. Filters: `status=open
PUT/api/admin/hnr/:idmodBody: `{ action: 'exempt'
GET/api/me/downloadsuserPer-torrent history with HnR state inline.

Implementation reference ​

ConcernFile
Schemapackages/db/src/schema.ts (hnr_tracking)
HnR logic (insert / update / mark)apps/api/utils/hnr.ts
Tracker-side delta updatesapps/tracker/internal/server/handler.go (announce path)
Mark-as-HnR sweeperapps/api/plugins/hnr-sweep.ts (checkAndMarkHnrs, every 10 min)
Moderator queueapps/api/routes/api/admin/hnr/*, apps/web/app/pages/mod/hnr.vue
User downloads pageapps/web/app/pages/downloads.vue

Released under the MIT License.