Skip to main content

FastAPI background tasks that survive a restart.

FastAPI’s BackgroundTasks runs in the same process as your request handler. If the worker dies or restarts, the task disappears without a retry, a schedule, or a record of the run.

What FastAPI’s BackgroundTasks does.

BackgroundTasks is built into FastAPI. It schedules a function to run after the response is sent, in the same process.

app.py
from fastapi import BackgroundTasks, FastAPI

app = FastAPI()

def send_report(user_id: str):
    rows = db.query(
        "SELECT * FROM orders WHERE user_id = %s", user_id,
    )
    report = generate_pdf(rows)
    smtp.send(to=user_id, attachment=report)

@app.post("/reports/{user_id}")
async def request_report(
    user_id: str, tasks: BackgroundTasks,
):
    tasks.add_task(send_report, user_id)
    return {"status": "queued"}
  • Runs after the response returns, in the same worker process.
  • Good for fast, fire-and-forget work: sending a confirmation email, logging an event.
  • No broker, queue, or separate worker to install.

Where it breaks.

BackgroundTasks is designed for trivial post-response work. Once the job needs to survive a restart or run on a schedule, the limits are concrete.

  • The task dies with the worker. A restart, a deploy, or an OOM kill drops it without a trace.
  • No retry. A failed HTTP call or a transient error is lost.
  • No schedule. There is no built-in way to run a task at a fixed interval.
  • No timeout control. A hung background task ties up the worker indefinitely.
  • No visibility. There is no log of what ran, what failed, or how long it took.
  • No concurrency control. Two requests can start the same background function twice with no deduplication.

qbash Tasks.

A Task is an AI automation written in Python, hosted on a managed runtime. A developer writes the script; anyone runs it from a form. Integrations, model calls, version control, and logging are built in.

Model and provider agnostic.

  • Anthropic
  • OpenAI
  • Gemini
  • xAI
  • OpenRouter
  • Amazon
  • Perplexity
  • MoonshotAI
  • Meta
  • Qwen

A durable alternative.

A qbash task runs on a managed runtime, triggered by a webhook, a cron schedule, or an API call. It survives restarts, retries on failure, and logs every run.

  • Durable runs up to 12 hours on a managed runtime, with no worker to keep alive.
  • Retry with checkpoints. A failure resumes where it left off, without re-firing side effects.
  • Cron schedules, webhooks, and API triggers. The same task starts by code or by clock.
  • Run logs with inputs, output, errors, and duration for every execution.
  • Call it from your FastAPI app over HTTP and get a synchronous or async response.
process.py
user = aisle.inputs.get("user_id")

rows = aisle.integrations.postgres.execute_query(
    "SELECT * FROM orders WHERE user_id = %(id)s",
    params={"id": user},
)

report = aisle.ai.run_prompt(
    slug="order-summary",
    variables={"orders": rows},
)

aisle.integrations.gmail.send_email(
    to=user, subject="Your order report",
    body=report["summary"],
)

Durable by default.

Runs survive restarts, failures retry from where they left off, and every execution is logged.

Checkpoints and retry

aisle.parallel records the items that finished. A retry resumes at the failure, so it does not pay again for model calls that already returned and does not re-fire side effects that already happened.

backfill.py
result = aisle.parallel(
    process_order, orders,
    concurrency=5, max_per_minute=60,
    checkpoint="order-backfill", retry=3,
)

Incremental processing

aisle.run.last_run_at tells you when the task last ran, so each execution processes only what is new. No state table to maintain.

sync.py
since = aisle.run.last_run_at

new_orders = aisle.integrations.postgres.execute_query(
    "SELECT * FROM orders WHERE created_at > %(since)s",
    params={"since": since},
)

for order in new_orders:
    aisle.integrations.pipedrive.create_deal(
        title=order["name"], value=order["amount"],
    )

Output goes where the work happens

Send results to Slack, email, a CRM field, or a database. 45+ integrations called in one line, with credentials brokered server-side.

deliver.py
aisle.integrations.slack.create_message(
    "#reports", text=summary,
)

url = aisle.files.download_url(report_file)

aisle.integrations.gmail.send_email(
    to="[email protected]",
    subject="Daily report",
    body=f"Download: {url}",
)

Questions

Why not use Celery instead?

Celery works, but it adds Redis or RabbitMQ as a broker, workers to keep alive, and retry logic to hand-roll. If the background work involves model calls, integrations, or file processing, a qbash task handles the runtime, the retries, and the provider bindings in one place.

Can I trigger a qbash task from my FastAPI app?

Yes. Each task has an API endpoint. Call it over HTTP from your request handler, the same way you would use BackgroundTasks, but the task runs on a separate, durable runtime.

Does qbash replace FastAPI?

No. qbash runs tasks, not web applications. Your FastAPI app stays where it is. Background work that needs to survive a restart, retry on failure, or run on a schedule moves to a task.

How long can a task run?

Up to 12 hours. Each run is logged with inputs, output, errors, and duration.

What does a managed runtime mean?

No server to provision, patch, or monitor. Saving the task deploys it. The runtime, the interpreter, and the upgrades are the platform’s responsibility.

Competitor details reviewed . Vendors change plans and features without notice, so check theirs before deciding.

Ship your first task.

Open the editor, write a script against your connected accounts, and put it on a trigger. Or describe it, and the builder drafts the task as code you edit.