FastAPI long running task: return 202 with a job id

Don't hold a FastAPI request open for a long task: return 202 with a job id. For an AI video job, Sume does the work and a webhook brings the result.

5 min readSume
All posts

For a long running task in FastAPI, don't keep the request open until the work ends. Accept the request, answer 202 Accepted with a job id, run the work outside the request, and let the client check a status route or receive a webhook. When the long task is an AI video on Sume's API, the work already runs on Sume's side: your endpoint submits the job, stores its id, and returns, with no loop in BackgroundTasks or a Celery worker waiting for the video.

FastAPI facts come from its Background Tasks, Response Status Code, and Concurrency and async / await pages; Sume facts come from Video Generation, Jobs and results, and Webhooks. All were read on 2026-09-27. Sume has no FastAPI package: the endpoint makes a plain HTTPS call with Requests.

Should I use BackgroundTasks or Celery for a long task?

It depends on where the work runs. FastAPI's docs describe BackgroundTasks as tasks run after returning a response, and even suggest answering 202 Accepted and processing a slow file in the background. For heavy computation that doesn't need the same process, they point to bigger tools like Celery, which tend to need a message or job queue manager such as RabbitMQ or Redis but can run tasks in multiple processes and servers.

A Sume video fits neither. Its docs say video generation typically takes 30 seconds to several minutes, all of it on Sume's side. A poll loop in BackgroundTasks would sit inside your web app for that long, and a Celery worker would only move the wait somewhere else.

From FastAPI's Background Tasks page and Sume's Video Generation docs, read 2026-09-27.
OptionWhere the work runsFits
BackgroundTasksIn the FastAPI app, after the response is sentSmall tasks, such as an email notification, or work that needs the app's own variables and objects
Celery with a queueWorker processes, possibly on other servers, fed by RabbitMQ or RedisHeavy computation you run yourself
An async API such as POST /v1/videosOn Sume's side; the submit returns a job id and polling URL at onceWork you only start and collect

How do I start the video job from a FastAPI endpoint?

Submit, store, return. The endpoint sets its own 202 with the decorator's status_code parameter. It is a plain def, as FastAPI's docs advise when a library that calls an API doesn't support await, and FastAPI runs such path operations in an external threadpool instead of blocking the server. Sume answers the submit with 202 and a bare object: id, polling_url, status: "pending", and model.

  • Idempotency-Key comes from your record, not from the request: resending the same key with the same body returns the original job instead of a second paid one.
  • callback_url must be a public HTTPS URL; Sume POSTs to it once the job reaches a terminal state. Localhost and private-network URLs are rejected.
  • Requests' docs point to json= when you need the application/json Content-Type, and warn that a call with no timeout can hang indefinitely.
  • SUME_API_KEY stays in the server's environment, never in frontend JavaScript.
import os, requests
from fastapi import FastAPI

app = FastAPI()

@app.post("/orders/{order_id}/video", status_code=202)
def start_video(order_id: str):  # plain def: runs in FastAPI's threadpool
    if job := db.find_video_job(order_id):  # your table: already submitted
        return job
    r = requests.post(
        "https://api.sume.com/v1/videos",
        headers={
            "Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
            "Idempotency-Key": f"order-{order_id}-video-v1",  # from the record
        },
        json={
            "model": "sume/auto",
            "prompt": db.video_prompt(order_id),  # same record, same body
            "callback_url": "https://example.com/hooks/sume",
        },
        timeout=30,
    )
    r.raise_for_status()
    job = r.json()  # id, polling_url, status: "pending"
    db.save_video_job(order_id, job["id"], job["polling_url"])
    return {"job_id": job["id"], "status": job["status"]}

What if the request times out or the server crashes?

Nothing is lost while the job id or the key survives. A submit is accepted the moment Sume has a durable job id, and the docs say to store that id so your integration can recover work after process restarts. A 2xx means the job exists and paid work is in flight, not that it finished.

  • Crashed before saving the id: resubmit with the same Idempotency-Key and the same body, and the replay returns the original job. A different body under the same key is 409 idempotency_conflict, so build the prompt from the record too.
  • Your own call timed out: a client-side timeout does not cancel the job. It keeps running and still bills, so read it back instead of submitting a new paid request.
  • Your worker restarted: the stored job id lets any process pick the job up again at GET /v1/jobs/{id}/status.

How does the finished video get back to my app?

Through callback_url. Sume sends terminal job events only, job.completed, job.failed, or job.canceled, signed with HMAC-SHA256 over <timestamp>.<raw_body>, and a job.completed payload lists artifacts with media.sume.com URLs. Verify the raw body before parsing it; Python webhook HMAC verification in FastAPI and Django has that route.

  • Store the event durably, then answer any 2xx. Each attempt gets 10 seconds, and Sume makes up to 10.
  • Use job_id as the idempotency key on your side, so a repeated delivery updates one row.
  • Keep polling as the backup for deliveries that never arrive. Text-to-video API in Python covers the poll loop and the download.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume