Skip to main content
Gated preview. Queued delivery is switched on per workspace. A workspace that is not enabled receives 403 with X-Comfy-Error-Type: not_enabled on the submit route. Nothing about the request is wrong and retrying will not change the answer. The same body works through the synchronous route in the meantime.
POST /v2/models/{provider}/{model} holds the connection until the model finishes. Queued delivery takes the same model ID and the same native request body, but returns as soon as Router has admitted the run. You get a request_id back at once and collect the result when it is ready, from the same process or another one. Use the queue when a generation can outlast the connection you can hold, when a web request has to return now, when you submit in one process and collect in another, or when you want many generations in flight at once. Ordering, admission, retries, timeouts, billing and expiry are all decided on the server. The SDKs add polling and ergonomics on top, nothing else.

Two delivery modes, one request

The SDKs (comfy-sdk and @comfyorg/sdk, 0.3.0 or later) expose the queue as three methods next to run:
  • submit(model, body) sends the request and returns a handle at once. The handle carries status(), get(), cancel() and an event iterator (iter_events() in Python, events() in TypeScript).
  • subscribe(model, body, ...) is submit, poll and collect in one call, with a progress callback.
  • handle(model, request_id) rebuilds a handle in another process from the two IDs, with no call made.
Both ids are needed everywhere because both address the request: the route is /v2/models/{provider}/{model}/requests/{request_id}.

The four routes

status is one of IN_QUEUE, IN_PROGRESS or COMPLETED. There is no separate failed or cancelled status: a request that did not succeed is COMPLETED carrying an error_type, so branch on the presence of that field, not on a fourth status value. The SDKs do this for you: get() raises or rejects with the typed Router error instead of handing the failure back as a result. The API reference carries the full contract for each route.

Queue a request

This queues the same request the quickstart sends and collects the image. Export your key as COMFY_API_KEY first.
Every model page carries this shape for its own model under Queue and collect later, beside the synchronous snippet.

Follow progress and collect in one call

When you do want to wait but also want to show progress, subscribe folds submit, poll and collect into one call:
The timeout is a client-side bound with no server-side meaning. When it runs out, subscribe makes one best-effort cancel before raising, so you are not paying for a generation nobody will collect. Use submit when the request should outlive the caller.

Collect from another process

Store the request_id next to the model ID. Both are needed to rebuild a handle, and no call is made until you use it.

Check status or cancel

status() is one poll and returns the current state. cancel() asks the server to stop a request that has not finished. It is a request, not a guarantee: a run already on the wire at the partner may complete anyway, and the next status() is what is true.

Async Python

AsyncComfy mirrors every name, argument and argument order. There is no submit_async, for the same reason there is no run_async.

Errors the SDKs raise

A request that finished without succeeding is reported as COMPLETED with an error_type. get() and subscribe() turn that into the typed Router error for the bucket: the classes in comfy_sdk.router_exceptions in Python, and routerErrors.* in TypeScript. The event iterator does not raise for that case, because it is a view of the queue’s progress: a completion carrying an error_type is yielded as the last observation, and get() is what collects. A 403 not_enabled on submit arrives as NotEnabled and is terminal, so the SDKs do not retry it.

What the responses look like

Submit, 201. status is always IN_QUEUE at this point. The three URLs are absolute and are authenticated with the same key as the submit.
request_id is also the value of the submit’s X-Comfy-Request-Id header. Keep the model ID next to it: the request is addressed by both. Status, 200. The same shape, with the current state. queue_position counts the requests ahead of yours and reaches 0 when the run is at the front. Retry-After on this response is Router’s estimate of when polling again is worth the round trip. It is a hint, not a bound, and a request at the back of the queue is told to wait longer than one already running. Polling faster learns nothing earlier and spends your own rate-limit allowance.
A request that finished without succeeding is COMPLETED with an error_type, carrying the same coarse bucket the result read puts on X-Comfy-Error-Type. The field is absent on success rather than null.
Result. 200 carries the model’s own native output, byte for byte what the synchronous route returns for the same model and input, under the provider’s own Content-Type. While the request is not finished the read answers 202 with the status body above, so a client that only polls the result URL parses one type. A request that failed comes back as an error response with X-Comfy-Error-Type set, the same buckets as the synchronous route. Cancel. 202 with CANCELLATION_REQUESTED means the ask was accepted, not that the run has stopped. A run already on the wire at the partner may complete anyway, and a partner generation that completes is charged whether or not anyone collects it. Read the status afterwards: a cancellation that took effect shows as COMPLETED with error_type: cancelled. A request that aged out before it could run shows queue_timeout the same way. A request that had already finished answers 409 with ALREADY_COMPLETED.

Idempotency and billing

  • Same charge as the synchronous route. You are billed when the provider bills Comfy. Time spent waiting in the queue is not charged.
  • One Idempotency-Key per submit. The SDK mints a fresh key per submit call, so two deliberate submits of the same input are two requests. A retry of the same call under the same key does not queue a second run: it returns the original handle with Idempotent-Replayed: true. Pass your own key when a lost response could have cost you the request_id. See Headers.
  • Results expire. A finished request is kept for 24 hours after it completes. After that, the status and result reads answer 410 and the result is gone. Collect promptly and download any asset URLs the output carries.
  • Polls are requests too. Status and result reads count towards the per-caller request rate. Honour Retry-After rather than polling on a fixed short interval.

Errors

Every error response carries X-Comfy-Request-Id. Quote it when you contact support.

Preview notes

The routes, fields and SDK methods on this page are the contract the preview runs against. Progress events, webhooks and priority are not part of it.

Next

Quickstart

The synchronous call for the same model, from nothing to an image.

Models

Every model page has the queued snippet for its own model and body.

Headers

Authentication, idempotency, request IDs, error buckets, retry pacing.

API reference

The four queue routes, field by field.