unslothai/unsloth · warning · HTTPException
The requested training job is no longer active.
Error message
The requested training job is no longer active.
What it means
HTTP 409 from the stop endpoint when the stop operation returns outcome=='superseded': the job identified by expected_job_id is no longer the active job. The stop helper checks the caller's expected_job_id against the backend's current job before stopping, so a stale client cannot stop a newer run it did not see.
Source
Thrown at studio/backend/routes/training.py:1768
):
"""
Stop the currently running training job.
Body:
save (bool): If True (default), save the model at the current checkpoint.
expected_job_id (str): Identifier of the job the caller intends to stop.
"""
try:
backend = get_training_backend()
outcome = await asyncio.to_thread(
_stop_training_if_active,
backend,
save = body.save,
expected_job_id = body.expected_job_id,
)
logger.info("Stop requested: save=%s outcome=%s", body.save, outcome)
if outcome == "superseded":
raise HTTPException(
status_code = 409,
detail = "The requested training job is no longer active.",
)
if outcome == "idle":
return TrainingStopResponse(
status = "idle", message = "No training job is currently running"
)
return TrainingStopResponse(
status = "stopped",
message = "Stop requested. Training will stop at the next safe step.",
)
except HTTPException:
raise
except Exception as e:
raise log_and_http_error(
e,View on GitHub (pinned to 203007d190)
Solutions
- Refetch the current training status to obtain the active job_id.
- If no training is running, treat the stop as already satisfied — call stop without expected_job_id or handle the 'idle' response.
- Only send expected_job_id when you genuinely intend 'stop exactly this run'.
Example fix
// before
await post('/training/stop', {save: true, expected_job_id: staleId})
// after
const status = await get('/training/status')
if (status.job_id) {
await post('/training/stop', {save: true, expected_job_id: status.job_id})
} Defensive patterns
Strategy: validation
Validate before calling
const status = await get('/training/status')
const target = status.job_id
if (!target) { console.log('No active job to stop'); return }
await post('/training/stop', {save: true, expected_job_id: target}) Try / catch
try { await post('/training/stop', body) } catch (e) { if (e.status === 409 && /no longer active/.test(e.detail)) { const s = await get('/training/status'); if (s.job_id) return post('/training/stop', {save: body.save, expected_job_id: s.job_id}) ; return } throw e } Prevention
- Always pair expected_job_id with a fresh status read in the same user action.
- Treat 'superseded' stop 409s as benign — the job you meant to stop is already gone.
- Handle the 'idle' response so normal no-op stops do not look like errors.
When it happens
Trigger: POST /training/stop with expected_job_id set to a job that already finished, was reset, or was replaced by a newly spawned training job.
Common situations: A UI that holds an old job id after a page reload or after another tab started a new run; automation polling a finished job and then issuing stop with the stale id.
Related errors
- Training job was superseded
- A transformers installation is replacing the latest sidecar;
- Training start request no longer owns the current job
- A transformers installation is replacing the latest sidecar;
- Training state changed during status read
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/7d96ccb9f06f977e.
Report an issue: GitHub.