Skip to content

Tracker service deployment

The tracker service has two deployment profiles. Both receive detections from an external detector and associate them into persistent tracks; neither image runs detector inference. Each stream/session URL owns one tracker instance.

Profile BoxMOT v24 image Rolling alias Frame pixels
CPU geometry boxmot/boxmot-service:24.0.0 boxmot/boxmot-service:latest Not required
CUDA/ReID boxmot/boxmot-service:24.0.0-gpu boxmot/boxmot-service:latest-gpu Required on every frame

The CPU service uses CPU-only Torch for canonical structures and contains no CUDA runtime. The GPU service uses CUDA 13.0 Torch and a shared ReID encoder. Neither service runs detector inference. The CPU profile supports ByteTrack, OcSort, and SFSORT; the GPU profile supports StrongSort, BotSort (default), DeepOcSort, HybridSort, BoostTrack, and OccluBoost.

Build and run

Run the published CPU image:

docker pull boxmot/boxmot-service:latest
docker run --rm -p 8000:8000 boxmot/boxmot-service:latest

Run the GPU image with the NVIDIA Container Toolkit and mount the ReID checkpoint at /models/osnet_x0_25_msmt17.pt:

docker pull boxmot/boxmot-service:latest-gpu
docker run --rm --gpus all -p 8000:8000 \
  -v "$PWD/models/osnet_x0_25_msmt17.pt:/models/osnet_x0_25_msmt17.pt:ro" \
  -e BOXMOT_SERVICE_REID_WEIGHTS=/models/osnet_x0_25_msmt17.pt \
  boxmot/boxmot-service:latest-gpu

Build either production target from the repository root:

docker build \
  --target service-cpu \
  -f docker/Dockerfile \
  -t boxmot/boxmot-service:local \
  .

docker build \
  --target service-gpu \
  -f docker/Dockerfile \
  -t boxmot/boxmot-service:local-gpu \
  .

docker run --rm -p 8000:8000 boxmot/boxmot-service:local

Check http://localhost:8000/healthz for liveness, http://localhost:8000/readyz for capacity, and http://localhost:8000/docs for the generated OpenAPI interface.

Send detections

Send exactly one request for each frame, including frames with no detections. The HTTP box_type fixes the session schema, the canonical TrackerSpec geometry, and the response layout before the first update. It also makes empty frames unambiguous. Non-empty row widths are validated against it. AABB is the HTTP default; OBB sessions must set "box_type": "obb".

The default AABB row is (x1, y1, x2, y2, confidence, class_id):

curl --request POST \
  --url http://localhost:8000/v1/streams/camera-01/sessions/run-01/frames \
  --header 'content-type: application/json' \
  --data '{
    "frame_id": 0,
    "width": 1920,
    "height": 1080,
    "frame_rate": 30,
    "box_type": "aabb",
    "detections": [
      [620.0, 210.0, 790.0, 690.0, 0.94, 0],
      [910.0, 240.0, 1050.0, 680.0, 0.88, 0]
    ]
  }'

For an empty frame, send "detections": [] with the next frame_id. For OBB, set "box_type": "obb" and use (cx, cy, width, height, angle_radians, confidence, class_id) rows.

The CPU profile does not require source image pixels; requests without an image must include both width and height. When image_base64 is supplied, the service infers dimensions from the decoded image. The GPU profile requires image_base64 to contain the raw base64 text of a valid JPEG or PNG for every request, including frames whose detections array is empty:

{
  "frame_id": 0,
  "frame_rate": 30,
  "box_type": "aabb",
  "detections": [[620.0, 210.0, 790.0, 690.0, 0.94, 0]],
  "image_base64": "<raw base64-encoded JPEG or PNG>"
}

Do not include a data:image/...;base64, prefix. Omit width and height when supplying an image; if provided, they must match the decoded image. ReID and camera-motion compensation need the real frame even when the detector found nothing, so an empty detection frame cannot omit image_base64.

For example, a Python client can attach a compressed frame without specifying its dimensions:

import base64
from pathlib import Path

import requests

payload = {
    "frame_id": 0,
    "frame_rate": 30,
    "box_type": "aabb",
    "detections": [[620.0, 210.0, 790.0, 690.0, 0.94, 0]],
    "image_base64": base64.b64encode(Path("frame.jpg").read_bytes()).decode("ascii"),
}
requests.post(
    "http://localhost:8000/v1/streams/camera-01/sessions/run-01/frames",
    json=payload,
    timeout=30,
).raise_for_status()

Base64 adds roughly 33% to the compressed image size and JSON adds a small additional cost. Prefer compressed JPEG where its quality is sufficient, and configure ingress request-body limits and timeouts for the resulting payloads. The service also validates compressed-byte and decoded-pixel limits described below.

The response includes a track_columns array that describes each dense track row. AABB output is (x1, y1, x2, y2, id, confidence, class_id, detection_index); OBB adds the angle before id. Use detection_index to relate a returned track to the corresponding input row. Track and detection counts are not guaranteed to be equal.

Frame dimensions, whether inferred or explicit, frame rate, and box type are fixed after the first request for a session. Frames must then be contiguous, with at most one request in flight for each session. A gap or conflicting retry returns HTTP 409 without advancing the tracker. Repeating the most recent frame with the exact same body safely replays its cached response. A new session must start with frame 0; this also prevents an expired or misrouted session from silently restarting midway through a sequence.

Delete a session to release its tracker immediately:

curl --request DELETE \
  http://localhost:8000/v1/streams/camera-01/sessions/run-01

Capture timestamps

The service defaults to fixed-step prediction (BOXMOT_VARIABLE_DT=false), matching established tracker tuning. The optional timestamp_s request field is metadata by default and does not change motion prediction.

To try experimental prediction in elapsed seconds, start the service with BOXMOT_VARIABLE_DT=true. This mode needs separate motion-noise calibration; existing benchmark accuracy is not guaranteed. Supply a finite capture or media timestamp in seconds on every frame, including frames without detections. For example, the CPU service accepts this first request:

{
  "frame_id": 0,
  "timestamp_s": 12.0,
  "width": 1920,
  "height": 1080,
  "detections": [[620.0, 210.0, 790.0, 690.0, 0.94, 0]]
}

The first timestamp establishes the session clock. A following request with "frame_id": 1 and "timestamp_s": 12.04 advances Kalman prediction by 0.04 seconds. The service passes the capture timestamp to the tracker, which computes the interval internally. GPU requests use the same field alongside their required image. Use capture or media timestamps rather than processing or network arrival times, and keep frame IDs contiguous even when capture intervals vary.

Timing mode is process configuration; request timestamps and frame_rate do not enable it. Request frame_rate is stream metadata; it does not scale ByteTrack or BotSort's max_lost_frames lifetime, which counts tracker updates. When enabled, a missing timestamp on a new session's first frame returns HTTP 422 before creating a tracker. Missing or non-increasing timestamps on subsequent frames return HTTP 409 without advancing the tracker. When disabled, optional timestamps may appear, disappear, or change without affecting prediction. In either mode, changing a timestamp on a retry returns HTTP 409; an exact retry of the most recent frame replays its cached response.

SFSORT rejects BOXMOT_VARIABLE_DT=true at service startup. All other service trackers support this option. When enabled, motion priors are converted to seconds using a fixed 1/30-second reference interval, then integrated over the actual capture interval. The reference does not replace capture timestamps; review elapsed-time prediction when tuning noise. Track expiration and confirmation settings remain counts of updates.

Configure the process

The CPU image defaults to ByteTrack; the GPU image defaults to BotSort. Their process-level settings are:

Variable CPU default GPU default Purpose
BOXMOT_SERVICE_PROFILE cpu gpu Selects the tracker allowlist and whether images/ReID are required. Use the profile built into the image.
BOXMOT_SERVICE_TRACKER bytetrack botsort CPU: bytetrack, ocsort, or sfsort. GPU: strongsort, botsort, deepocsort, hybridsort, boosttrack, or occluboost.
BOXMOT_SERVICE_ASSO_FUNC iou iou Geometry used for AABB or OBB detection-track matching: iou, giou, diou, ciou, hmiou, or centroid.
BOXMOT_VARIABLE_DT false false Opt into experimental elapsed-seconds prediction; requires timestamp_s on every frame and separate motion-noise calibration. Unsupported by SFSORT.
BOXMOT_SERVICE_DEVICE cpu 0 ReID device passed to the GPU backend.
BOXMOT_SERVICE_HALF false true Enables FP16 ReID inference; relevant to the GPU profile.
BOXMOT_SERVICE_REID_WEIGHTS Not used /models/osnet_x0_25_msmt17.pt Mounted ReID checkpoint path.
BOXMOT_SERVICE_PORT 8000 8000 Internal HTTP port.
BOXMOT_SERVICE_MAX_STREAMS 256 256 Maximum resident tracker sessions.
BOXMOT_SERVICE_STREAM_TTL_SECONDS 900 900 Idle time before an unlocked session can be evicted.
BOXMOT_SERVICE_MAX_DETECTIONS 1000 1000 Per-frame detection limit; configurable up to 2000.
BOXMOT_SERVICE_MAX_CLASSES 32 32 Maximum distinct class IDs retained by one session.
BOXMOT_SERVICE_MAX_CONCURRENT_UPDATES 4 1 Process-wide tracker updates. Keep the GPU default when sharing one ReID model.
BOXMOT_SERVICE_MAX_IMAGE_BYTES 20000000 20000000 Maximum compressed image size after base64 decoding.
BOXMOT_SERVICE_MAX_FRAME_PIXELS 33177600 33177600 Maximum decoded pixel count when an image is supplied.

For example:

docker run --rm \
  -p 8080:8080 \
  -e BOXMOT_SERVICE_PORT=8080 \
  -e BOXMOT_SERVICE_TRACKER=ocsort \
  -e BOXMOT_SERVICE_ASSO_FUNC=centroid \
  -e BOXMOT_SERVICE_MAX_STREAMS=512 \
  boxmot/boxmot-service:latest

Centroid normalization uses the session's fixed width and height. The CPU profile does not require source image pixels when centroid is selected.

For OBB sessions, iou uses oriented-rectangle overlap, giou uses the joint convex hull, and diou/ciou use the rotation-invariant minimum-area joint oriented enclosure for center-distance normalization. OBB ciou is a custom experimental long/short-side aspect adaptation. OBB hmiou is an experimental product of oriented IoU and global-y projection IoU and is intended only for scenes where image vertical is a meaningful height or depth cue. The association function guide defines every OBB mode and its score normalization.

Scale replicas

Run one Uvicorn worker per container. Tracker objects are mutable and live in that process, so all requests for the same stream/session must reach the same replica. Configure the load balancer for sticky routing or consistent hashing on the stream/session key, then scale by adding containers.

The GPU process loads and warms one ReID model, then shares it across all of that process's tracker sessions. Its default concurrency is therefore one. Keep one worker process per GPU container unless model memory, inference synchronization, and capacity have been explicitly designed for a different layout. Add GPU containers to scale and retain sticky stream/session routing.

A container restart discards its sessions. Start a new session ID and replay the sequence from its first required frame if state must be reconstructed. Persistent shared storage alone cannot move a live tracker object between workers.

Both service images include Torch because HTTP rows are decoded into canonical structures before tracking. The CPU image uses a CPU-only build and omits CUDA and perception model runtimes. The GPU image instead uses the locked CUDA 13.0 build. Its process owns one shared appearance encoder and enriches requests before advancing per-stream trackers; injected segmentors follow the same engine-owned pattern. Neither profile runs a detector. Use boxmot/boxmot:24.0.0 for full GPU CLI/detector workflows or boxmot/boxmot:24.0.0-cpu for their CPU counterpart. Those CLI images bundle native ReID and all five supported live C API tracker libraries; the service images use Python tracker backends. Place authentication, TLS, request-size limits, and rate limiting at the ingress or API gateway.

Run persistent CLI jobs

Materialization and evaluation are batch CLI workloads, not service endpoints. Keep the raw dataset, immutable build, and model directories outside their containers:

mkdir -p "$PWD/datasets/mot" "$PWD/runs/materializations" "$PWD/models"

docker run --rm --gpus all --ipc=host \
  -v "$PWD/datasets/mot:/opt/boxmot/datasets/mot" \
  -v "$PWD/runs/materializations:/materializations" \
  -v "$PWD/models:/opt/boxmot/models" \
  -e BOXMOT_BUILDS_DIR=/materializations \
  boxmot/boxmot:24.0.0 \
  boxmot materialize \
    --experiment mot17/ablation-yolox-lmbn.yaml \
    --device 0

BUILD_ID=replace-with-the-64-character-build-id

docker run --rm --gpus all --ipc=host \
  -v "$PWD/datasets/mot:/opt/boxmot/datasets/mot:ro" \
  -v "$PWD/runs/materializations:/materializations:ro" \
  -v "$PWD/models:/opt/boxmot/models:ro" \
  -e BOXMOT_BUILDS_DIR=/materializations \
  boxmot/boxmot:24.0.0 \
  boxmot eval \
    --experiment mot17/ablation-yolox-lmbn.yaml \
    --build "$BUILD_ID" \
    --tracker occluboost

Using the same mount points preserves source references and lets an ID passed to --build resolve under BOXMOT_BUILDS_DIR. Built-in artifact paths are relative to /opt/boxmot, which is why the model volume is mounted there. For CPU jobs, use boxmot/boxmot:24.0.0-cpu, remove --gpus all, and materialize with --device cpu.