{"record":{"id":"800ed09df563db4d","repo":"langflow-ai/langflow","slug":"checkpoint-expired-or-not-found-cannot-resume-thi","errorCode":null,"errorMessage":"Checkpoint expired or not found; cannot resume this run.","messagePattern":"Checkpoint expired or not found; cannot resume this run\\.","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"src/backend/base/langflow/api/build.py","lineNumber":481,"sourceCode":"    async def build_resumed_graph_and_get_order() -> tuple[list[str], list[str], Graph]:\n        \"\"\"Resume a suspended HITL run from its durable checkpoint instead of building fresh.\n\n        Hydrates the graph, injects the human decision keyed by request_id, and un-builds\n        the paused node so it re-runs and routes (its first-run output was a placeholder).\n        \"\"\"\n        from lfx.graph.graph.base import Graph as LfxGraph\n        from lfx.run.hitl import request_id_targets_vertex\n        from lfx.services.deps import get_checkpoint_service\n\n        from langflow.api.v2.hitl import reroute_decision_on_timeout\n\n        run_id = str(job_id)\n        store = get_checkpoint_service()\n        checkpoint = await store.load_by_run_id(run_id)\n        if checkpoint is None:\n            # Why: re-dispatching here (resume/job_id still set) recurses to RecursionError; a missing\n            # or expired checkpoint is unrecoverable, so surface a clean 404 instead.\n            raise HTTPException(status_code=404, detail=\"Checkpoint expired or not found; cannot resume this run.\")\n        graph = LfxGraph.resume_from_checkpoint(checkpoint, checkpoint_store=store)\n        if not graph.user_id:\n            graph.user_id = str(current_user.id)\n        # Resume skips the initial run's trace setup (trace_context_var stays unset → post-pause\n        # vertices like Chat Output never trace); re-init so the resumed vertices trace.\n        graph.flow_name = graph.flow_name or flow_name\n        await graph.initialize_run()\n        pending = await get_job_service().get_pending_human_request(job_id)\n        decision = reroute_decision_on_timeout(pending, resume[\"decision\"])\n        # Merge with checkpoint-restored decisions so a re-run HITL keeps its answer (no multi-HITL loop).\n        graph.human_input_decisions = {\n            **(getattr(graph, \"human_input_decisions\", {}) or {}),\n            resume[\"request_id\"]: decision,\n        }\n        action_id = str((decision or {}).get(\"action_id\", \"\"))\n        gate_label = _hitl_gate_label(action_id, (pending or {}).get(\"options\"))\n        if graph.tracing_service:\n            graph.tracing_service.record_event_span(","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/build.py#L463-L499","documentation":"Raised as HTTP 404 when a HITL (human-in-the-loop) resume request references a run whose checkpoint no longer exists in the checkpoint store. The comment in the source explains why: re-dispatching with resume/job_id still set would recurse to RecursionError, so a missing or expired checkpoint is treated as unrecoverable and surfaced as a clean 404.","triggerScenarios":"Calling the resume endpoint (POST with a resume payload and job_id) for a run whose checkpoint was evicted, expired, or never persisted; restarting the server with an in-memory checkpoint store; resuming after the checkpoint TTL elapsed.","commonSituations":"Long-paused HITL runs, server restarts wiping in-memory checkpoints, multiple replicas with non-shared checkpoint storage, or retrying a resume after the underlying job was already completed or cleaned up.","solutions":["Check the run's checkpoint exists before offering a resume button (GET pending human request / checkpoint lookup)","If checkpoints are in-memory, switch to a persistent checkpoint service (database-backed) so restarts do not lose state","Resume promptly or raise checkpoint TTL so paused runs survive","If already unrecoverable, start a new run instead of resuming; the 404 is intentional to avoid infinite re-dispatch recursion"],"exampleFix":"// before\nawait client.resumeRun(jobId, { decision: 'approve' }); // 404 after restart\n// after\nconst pending = await client.getPendingRequest(jobId);\nif (!pending) {\n  // checkpoint gone: start a fresh run\n  await client.runFlow(flowId);\n} else {\n  await client.resumeRun(jobId, { decision: 'approve' });\n}","handlingStrategy":"validation","validationCode":"const pending = await client.getPendingHumanRequest(jobId);\nconst cp = await checkpointService.loadByRunId(jobId);\nif (!pending || !cp) { /* start a new run, do not resume */ }","typeGuard":null,"tryCatchPattern":"try { await resumeRun(jobId, decision) } catch (e) { if (e.status === 404 && /Checkpoint expired/.test(e.detail)) { await startNewRun(); } else throw e; }","preventionTips":["Use a persistent (DB-backed) checkpoint store in multi-replica deployments","Check pending human request + checkpoint existence before showing a resume UI","Resume HITL requests promptly; treat checkpoint TTL as a hard SLA"],"tags":["hitl","checkpoint","http-404","state-management"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}