unslothai/unsloth · warning · HTTPException

Training start request no longer owns the current job

Error message

Training start request no longer owns the current job

What it means

HTTP 409 when cancel_start_request returns outcome 'superseded': the start request being cancelled no longer owns the current job (a newer start request has taken over the job slot), so cancelling it would not stop anything.

Source

Thrown at studio/backend/routes/training.py:1122

async def cancel_training_start_request(
    start_request_id: str = ApiPath(
        ...,
        min_length = 1,
        max_length = 128,
        pattern = TRAINING_REQUEST_ID_PATTERN,
    ),
    current_subject: str = Depends(get_current_subject),
):
    backend = get_training_backend()
    try:
        outcome, record = await asyncio.to_thread(
            backend.cancel_start_request,
            start_request_id,
        )
    except TrainingStartCancellationCapacityError as exc:
        raise HTTPException(status_code = 429, detail = str(exc)) from exc
    if outcome == "superseded":
        raise HTTPException(
            status_code = 409,
            detail = "Training start request no longer owns the current job",
        )
    return _start_request_status_response(record)


def _background_video_generation_active() -> bool:
    """Whether a video clip is generating on the video backend's worker thread.

    POST /video/generate returns at once and generates in the background, so an
    in-flight clip is invisible to the keep-warm in-flight request count the
    API-key training guards consult; ask the backend directly. Best-effort: a
    probe failure must never block a training start."""
    try:
        from core.inference.video import get_video_backend
        return bool(get_video_backend().generate_progress().get("active"))
    except Exception as e:  # noqa: BLE001
        logger.warning("Could not check video generation state for training guard: %s", e)

View on GitHub (pinned to 203007d190)

Solutions

  1. Fetch the current job id from the backend (job/status endpoints) and cancel via the current owning start request
  2. If the goal was 'stop whatever is training', cancel the newest start request or the current job directly
  3. Refresh cached start_request_id after every new /training/start call

Example fix

// before
client.post(f"/training/start-requests/{old_id}/cancel")  # 409 superseded

// after
job = client.get("/training/current-job").json()
owner_id = job["start_request_id"]
client.post(f"/training/start-requests/{owner_id}/cancel")
Defensive patterns

Strategy: validation

Validate before calling

def cancel_is_meaningful(status: dict) -> bool:
    # only cancel the request that currently owns the job
    return status.get("state") not in ("superseded", "completed", "failed", "cancelled")

Try / catch

resp = client.post(f"/training/start-requests/{rid}/cancel")
if resp.status_code == 409 and "no longer owns" in resp.text:
    current = client.get("/training/current-job").json()
    rid = current["start_request_id"]  # cancel the actual owner

Prevention

When it happens

Trigger: POST cancel on an old start_request_id after a newer training start request has already replaced it as the owner of the current job.

Common situations: Client holds a stale id after re-submitting training start; concurrent automation where one flow starts a new job while another tries to cancel the old request.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/789963a99a0b4a61. Report an issue: GitHub.