Skip to lesson

NVIDIANVIDIA-Certified Professional: Agentic AI2026-104.1 Deploy and orchestrate multi-agent systems at production scale.

Recover distributed tasks from persisted progress

Completed by the knowledge check

Learning objective

Coordinate distributed agent tasks with queues, timeouts, and durable state.

Core idea

A background job needs more than a detached HTTP request. A scheduler decides when workers run it; a deadline decides whether it is still useful; persisted state records what the system can recover after a process fails. Design all three together.

NeMo Agent Toolkit’s asynchronous endpoint combines Dask execution with SQL-backed job history and metadata. That is a concrete starting point, but job records and resumable agent execution are different responsibilities. Here the question is how distributed work remains bounded and recoverable when workers and schedulers fail independently.

How it works

Submit an identified job, then schedule its work

Install nvidia-nat[async_endpoints] to enable the asynchronous workflow endpoint. The API server documents /v1/workflow/async, with legacy /generate/async. Versioned workflow paths are marked experimental because of HTTP human-in-the-loop and OAuth support; the documented compatibility with legacy paths covers workflows that do not rely on those behaviors.

A request can supply job_id. If omitted, the server generates a UUID; if supplied, uniqueness is the caller’s responsibility. When that identifier already exists, the endpoint returns the job’s status. Reusing a known identifier after a lost submission response is therefore different from submitting another identifier and potentially starting another workflow. This identifier behavior does not make every external action inside the workflow execute exactly once; see 2.4.4 for side-effect recovery.

The endpoint uses Dask for asynchronous jobs. Connect to an external scheduler with nat serve --scheduler_address SCHEDULER_ADDRESS, replacing SCHEDULER_ADDRESS with its network address. This separates API service replacement from the scheduler’s lifecycle and creates a separately owned dependency.

Dask distinguishes tasks waiting on dependencies from runnable root tasks queued because workers are full. Its scheduler state documentation calls those internal states waiting and queued, respectively. Treat these as diagnostic concepts, not a stable application API: that page explicitly identifies scheduler internals as subject to change. A queue can absorb a burst, but it cannot increase the rate at which worker and inference resources complete jobs.

For local clusters, --max_concurrent_jobs is documented as a misleading name for the maximum worker count. It and --dask_threads_per_worker do not apply when scheduler_address is set. Configure and measure the external cluster’s capacity instead.

Separate response waiting from useful execution time

The request field sync_timeout controls how long the submission waits for completion. Its default is 0, returning immediately; its maximum is 300 seconds. If work finishes inside the waiting interval, the response includes the result; otherwise it returns job_id and status. A short wait changes the client interaction, not the job’s allowed execution duration. The submission returns the result only when work finishes inside the waiting interval, and reusing a known job_id returns the job’s status.

The submission returns the result only when work finishes inside the waiting interval, and reusing a known job_id returns the job's statusThe caller submits a request with job_id and sync_timeout. The API server hands the asynchronous job to the Dask scheduler. If work finishes inside the waiting interval, the response includes the result, and otherwise it returns job_id and status. After a lost submission response, the caller reuses the known identifier and receives the job's status rather than starting another workflow.Dask schedulerAPI servercalleralt[inside the waitinginterval][otherwise]lost submission responsejob_id, sync_timeoutasynchronous jobthe resultjob_id and statusreuse a known identifierthe job's status

Likewise, expiry_seconds controls preservation of output files after completion. It defaults to 3600, has a minimum of 600 and a maximum of 86400, and does not affect the response’s text output. It is neither queue expiration nor execution cancellation.

Define an application deadline covering queue wait and execution. Store it with the job request, and check remaining time before expensive work. Apply that remaining budget with asyncio.timeout() inside the Python worker; see 1.8.2 for cancellation and timeout handling. A rescheduled task inherits the accepted deadline instead of receiving a fresh execution budget.

An execution timeout does not undo a remote action already performed. Keep the timeout outcome distinct from confirmed failure of that action. The recovery rules in 2.4.4 determine whether an uncertain side effect can be replayed; adding another queue delivery does not resolve that uncertainty.

Persist both job records and resumable computation

See 1.8.3 for persistent job storage through --db_url and external scheduling through --scheduler_address. Those settings separate job metadata from the scheduler’s computation records; recovery must account for both.

Dask documents two distinct failure cases. After worker loss, it can reroute pending computations and recompute missing results from the task graph. If the scheduler itself dies, its ongoing computation records are lost: the resilience documentation states that scheduler state has no persistence-and-recovery mechanism. A surviving SQL job row is therefore insufficient proof that its computation continues after scheduler restart.

See 1.8.3 for graph checkpointers, configurable.thread_id, and the "sync" versus "async" durability trade-off. For long jobs, durability="exit" cannot preserve intermediate progress through a process crash because it saves only when execution exits.

Account for the invocation boundary: the Toolkit v1.8.0 implementation of langgraph_wrapper calls graph.ainvoke() and graph.astream() without passing a RunnableConfig. Its 1.8 documentation’s State Management section warns that complex state management may require code modifications. For the recovery path here, the application worker invokes the checkpointed graph directly with an explicit configurable.thread_id; it does not use the unmodified wrapper or assume that async job_id supplies thread identity.

Make reconciliation an application procedure with explicit records:

  1. Read the application’s durable admission record, containing job_id, the chosen thread_id, the accepted deadline, and its terminal outcome if one exists. These are application fields, not additional Toolkit request fields. Compare that record with SQL job history to identify submissions without a confirmed terminal result.
  2. For unfinished work, check the deadline before scheduling recovery. If it has passed, record an application-level expired outcome and notify the caller; do not enqueue more inference under the original commitment.
  3. For eligible work, load the saved graph state for that thread_id and check completed stages and unresolved external effects. Apply 2.4.4’s replay rules before allowing another attempt; a scheduler’s missing task record does not establish that an external action failed.
  4. Have one recovery owner resubmit eligible work, invoke the graph with the same recorded thread identifier, and retain the resulting outcome in the admission record. Verify completion before closing the job. Reusing job_id at the async endpoint retrieves status; it is not a command to restart an existing unfinished job.

Reconciliation joins application admission, job history, and graph progress; restarting the scheduler does not perform it.

Key distinctions and trade-offs

The mechanisms answer different questions:

ConcernMechanismWhat it establishesRemaining cost or risk
Work admission and schedulingDask scheduler and workersWhich available worker executes runnable workQueue delay and finite worker capacity
HTTP response waitsync_timeoutHow long submission waits for a resultBackground work can continue afterward
Useful execution budgetApplication deadline and asyncio.timeout()When a worker stops waiting on an operationRemote effects can remain uncertain
Job history--db_urlPersistent job metadataScheduler computation is separate
Agent progressPersistent checkpointer, thread_id, durability modeSaved graph state for recoveryDatabase latency and safe replay requirements

Architecture / decision scenario

A legal research service receives 24 document-analysis jobs together. Its measured workload takes 120 seconds per job with eight execution slots, and users require completion within 300 seconds of acceptance. Once the 90-second extraction stage is checkpointed, recovery must preserve it rather than repeat it. Under these simplified, measured conditions, the third wave finishes at 360 seconds. Returning immediately with sync_timeout: 0 cannot repair that missed completion target.

The service owner admits 16 jobs for the five-minute commitment and offers the remaining eight a separately agreed later window. Its durable admission record stores job_id, the graph thread_id selected by the application, and the accepted deadline. Workers check it before analysis, and bound awaited calls with the remaining execution budget. The platform owner operates the external Dask scheduler; the API uses --scheduler_address, while job history uses persistent --db_url storage.

The tempting recovery design is to restart the scheduler and trust PostgreSQL job rows to resume everything. That fails the requirement to preserve completed analysis stages. The workflow team instead uses its own worker invocation of the PostgreSQL-checkpointed graph with durability="sync", accepting its measured write latency. The worker explicitly passes the configurable.thread_id from the admission record. After scheduler loss, the recovery owner reads unfinished admission and job records, expires overdue work, and resumes eligible graph threads from their saved state. The team tests scheduler loss separately from worker loss because Dask’s guarantees differ for the two.

Common exam traps

  • Treating immediate asynchronous acceptance as a completion-time guarantee.
  • Reading sync_timeout as an execution deadline or expiry_seconds as queue expiration.
  • Applying local Dask worker flags to an external scheduler.
  • Treating a persistent SQL job row as persistent scheduler state.
  • Assuming worker recomputation makes arbitrary business actions safe to repeat.
  • Assuming Toolkit 1.8’s unmodified langgraph_wrapper passes the thread identity required by a checkpointer.

What to remember

Identify the job, bound queue wait and execution, and persist the state needed for recovery. Dask schedules work, SQL records job metadata, and a persistent graph checkpointer preserves resumable agent progress. Response waiting, artifact retention, and execution deadlines remain separate controls.

Knowledge check

References