---
title: "Durable Background Runs"
description: "Long agent-chat turns can run on a separate 15-minute Netlify background function instead of the synchronous request, so multi-step work finishes server-side without the client babysitting continuations. Enabled by default on deployed Netlify apps with an explicit opt-out; a foreground circuit-breaker keeps chat working even if the worker never starts."
---

# Durable Background Runs

<Callout id="doc-block-dbgr1who" tone="info">

**Who is this for:** anyone deploying an agent-native app who wants long,
multi-step agent turns to complete server-side, and anyone wondering what the
`AGENT_CHAT_DURABLE_BACKGROUND` flag and the second Netlify function do. This
is enabled by default on deployed Netlify apps and entirely inside the
framework — there is no app code to write.

</Callout>

A normal hosted agent turn runs inside the synchronous request function, which
is capped by the platform's request timeout (60s on Netlify, and
the framework's ~40s soft-timeout before it has to hand back to the client and
"continue from where you left off"). That is fine for quick answers, but a long
agentic turn — many tool calls, large analyses — ends up bouncing through
several client-driven continuations.

Durable background runs move that long turn onto a **separate Netlify
background function** with a **15-minute** budget. The turn runs to completion
server-side in one invocation; the UI just streams the result.

## Agents that survive any host {#survive-any-host}

An agent run does not live in the function that started it. Each run is a row
in your database with a heartbeat, plus an event log of everything the agent
streamed. That one design choice is what lets agents run on ephemeral hosts:

- **A turn can outgrow its function.** When a function nears its time limit
  (the ~40s budget of a hosted request, or the 15-minute budget of a Netlify
  background function), the turn stops at a safe point and continues in a new
  invocation from the saved conversation. A tool-call journal records what
  already ran, so continuing never repeats a write. See
  [Durable resume](/docs/durable-resume).
- **The browser can lose its connection.** A reconnecting chat, or one being
  served by a different function than the one doing the work, streams from the
  run's event log, so the reply picks up where it left off.
- **A function can die without warning.** Its heartbeat stops, the run is
  reclaimed after a short stale window, and it is either recovered or ended
  with a clear, retryable error instead of staying "running".

All it needs is a PostgreSQL database. The same agent code runs on every
supported target: Node.js, Docker, Vercel, Netlify, AWS Lambda, and Cloudflare.
Hosts with longer function budgets simply need fewer continuations; on Netlify
that is the background function described below.

This is why hosting agents here is unusually cheap and portable. There is no
always-on agent server, queue, or per-user sandbox, so you pay for compute only
while an agent is working. Agent code does not change between hosts, so you can
switch providers without a rewrite. And because the agent and the UI share the
same actions and database, the deployment that hosts your agents can host the
rest of your app too.

## Two functions, not one {#two-functions}

A deployed app has exactly two server functions, and almost every request hits
the first one. Only the dispatched agent turn runs on the second.

<Diagram id="doc-block-1qxzr96" title={"server handles everything; only the dispatched agent turn runs on the background function"} summary={"The synchronous server function serves pages, actions, API, and the foreground chat POST; it HMAC-dispatches the long turn to the background function, which has the 15-minute budget."}>

```html
<div class="diagram-bg-runs">
  <div class="diagram-panel" data-rough>
    <div class="diagram-pill">server &middot; synchronous</div>
    <div class="diagram-box">Pages / SSR</div>
    <div class="diagram-box">Actions</div>
    <div class="diagram-box">API + framework routes</div>
    <div class="diagram-box">
      Foreground chat POST
      <small class="diagram-muted">decides + dispatches</small>
    </div>
    <small class="diagram-muted"
      >60s timeout &middot; nearly every request</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">
    &rarr;<br /><small>HMAC 202</small>
  </div>
  <div class="diagram-panel" data-rough>
    <div class="diagram-pill accent">
      server-agent-background &middot; background:true
    </div>
    <div class="diagram-box">_process-run worker</div>
    <div class="diagram-box">Agent turn: LLM + tool calls</div>
    <small class="diagram-ok"
      >15-min budget &middot; only the dispatched turn</small
    >
  </div>
</div>
```

```css
.diagram-bg-runs {
  display: flex;
  align-items: stretch;
  gap: 14px;
  flex-wrap: wrap;
}
.diagram-bg-runs .diagram-panel {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px;
  flex: 1;
  min-width: 230px;
}
.diagram-bg-runs .diagram-box {
  padding: 8px 10px;
}
.diagram-bg-runs .diagram-arrow {
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  min-width: 90px;
  font-size: 20px;
}
```

</Diagram>

There is **nothing to flag in app or template code.** Netlify treats a function
as a background function when its `export const config` has `background: true`
(or its name ends `-background`). The framework's build step emits the
`server-agent-background` wrapper automatically when the feature is enabled —
app authors never declare it.

## What runs in the background, and how it's chosen {#what-runs}

Only the long agent-chat turn — the internal `_process-run` dispatch that
re-enters the agent loop (LLM calls + tool execution). Pages, actions, and
normal API calls always run synchronously on `server`.

The foreground chat POST decides **per turn**. It dispatches to the background
function only when all three hold:

- the app is deployed on Netlify and `AGENT_CHAT_DURABLE_BACKGROUND` is not
  explicitly falsy (`false` / `0` / `no` / `off`), or the app explicitly opts
  in with a truthy value (`true` / `1` / `yes` / `on`),
- the app is running hosted (on the platform, not local dev), unless it opted
  in explicitly, and
- `A2A_SECRET` is set (the dispatch is an HMAC-signed self-call; without the
  secret it cannot be signed, so the turn stays inline).

If the app is local without an explicit opt-in, `A2A_SECRET` is missing, or the
flag is explicitly disabled, the turn runs inline exactly as before. An explicit
opt-in is honored in local development too, so leave the flag unset there to
keep turns inline. The setting is read at **both build time** (to decide
whether to emit the background function) and **runtime** (to decide whether to
dispatch), so changing it requires a redeploy.

## Long-lived Node servers {#long-lived-servers}

A container or VM running `agent-native start` has no second function. With
`AGENT_CHAT_DURABLE_BACKGROUND=true` and `A2A_SECRET` set, the foreground
dispatches the turn to the framework's signed `_process-run` route on the same
deployment, so the turn runs in its own request instead of the one that started
it. The run's time budget does not change: it is still decided by whether the
runtime is a hosted platform.

The dispatch goes to the deployment's own public URL by default (the platform
URL, then `APP_URL`), which is a round trip through the host's edge. Set
`AGENT_NATIVE_SELF_DISPATCH_URL` to the address this process listens on, such
as `http://127.0.0.1:3000`, to dispatch over loopback instead. It must be an
absolute `http(s)` URL, and the app base path is appended to it. It only
addresses requests the deployment sends to itself, never a URL an outside
service calls.

## The lifecycle and the circuit-breaker {#lifecycle}

A Netlify async function returns `202` the instant it **enqueues** the
invocation — that is not proof the worker executed. So the foreground does not
blindly trust the `202`: after dispatching it briefly polls for the worker to
actually **claim** the run, and recovers inline if it doesn't.

<Diagram id="doc-block-1x98xgk" title="The foreground always returns a working turn" summary={"On dispatch the foreground waits a grace window for the worker to claim the run; if it claims, the foreground subscribes to it; if not, the foreground claims and runs the turn inline."}>

```html
<div class="diagram-bg-life">
  <div class="diagram-box" data-rough>Foreground POST</div>
  <div class="diagram-arrow diagram-muted">&rarr;</div>
  <div class="diagram-box" data-rough>
    Insert run + HMAC dispatch &rarr; 202
  </div>
  <div class="diagram-arrow diagram-muted">&rarr;</div>
  <div class="diagram-panel center" data-rough>
    <strong>Worker claims within the grace?</strong
    ><small class="diagram-ok"
      >yes &rarr; stream the background worker (up to 15 min)</small
    ><small class="diagram-warn"
      >no &rarr; circuit-breaker recovers the turn inline</small
    >
  </div>
</div>
```

```css
.diagram-bg-life {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.diagram-bg-life .diagram-arrow {
  font-size: 20px;
}
.diagram-bg-life .center {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px;
}
```

</Diagram>

The claim is observed via `dispatch_mode` flipping from `background` to
`background-processing` (only the worker's atomic claim does that). The grace
window must cover the worker's cold-start; heavy apps were observed needing
longer than the original 8s, so it is **15s** (still well within the
foreground's ~40s soft-timeout). A worker that never claims — dead, or just
slow to start — falls back to inline after the grace, so the turn still
completes; the cost is the added latency of waiting out the grace first.

<Callout id="doc-block-1gfqe9e" tone="success">

The foreground returns a working turn either way: the background worker when it claims in time, an inline turn otherwise. So durable runs should preserve chat correctness even when a worker fails — but a misconfigured or slow-to-claim worker can add fallback latency (up to the grace window) before the inline recovery takes over.

</Callout>

## Cost and tradeoffs {#cost}

Netlify background functions are available on every credit-based Netlify plan,
including Free, and on Enterprise. They bill as functions compute, the same
meter as the synchronous function: 10 credits per GB-hour of runtime at the
function's memory size (1 GB by default). The Free plan's 300 monthly credits
are a hard cap, and Netlify pauses the team's projects when they run out.
Legacy plans (accounts created before September 4, 2025) bill functions per
request, and Legacy Free and Starter don't include background functions; see
Netlify's
[functions usage and billing](https://docs.netlify.com/build/functions/usage-and-billing/)
page.

A multi-minute agent turn therefore costs more compute than a short synchronous
call — but it largely **replaces** the old pattern, where the turn hit the ~40s
wall and the client re-issued several continuation requests. It is the same
work consolidated into one long invocation, not purely additive.

<Callout id="doc-block-gjoj2d" tone="info">

Durable runs are enabled by default for deployed Netlify apps. Set
`AGENT_CHAT_DURABLE_BACKGROUND=false` to opt out, or set it to `true` on another
host. `A2A_SECRET` is still required. Redeploy after changing the setting, and
verify a turn actually used the worker by checking that its run reaches
`dispatch_mode=background-processing` rather than falling back to inline
recovery.

</Callout>

## The foreground self-chain: a narrower, opt-in escape hatch {#foreground-self-chain}

A second, independent flag governs a different continuation mechanism:
`AGENT_CHAT_FOREGROUND_SELF_CHAIN`. Where durable background runs move a whole
long turn onto the 15-minute background function above, the foreground
self-chain lets a normal (non-durable-background) turn that hits its
soft-timeout chunk boundary continue via a server-side self-dispatch on the
**regular** request function, instead of depending on the client to re-POST
`auto_continue`.

It is true only when the env flag is explicitly truthy (`true` / `1` / `yes` /
`on`), the runtime is hosted, and `A2A_SECRET` is configured. Unlike
`AGENT_CHAT_DURABLE_BACKGROUND`, an explicit flag does not lift the hosted
requirement. The two flags are independent and
never need to agree — an app can enable this narrower capability without
opting into the full background-function worker path. When a run already
qualifies for durable background dispatch, that decision is evaluated first
and takes precedence; the run chains through the background worker instead of
this path.

<Callout id="doc-block-fgschn1" tone="warning">

This flag is **opt-in and default OFF**. A regular Netlify function has a
fixed **60-second** wall — far shorter than the 15-minute background-function
budget above — so a self-dispatched successor can be killed mid-run before it
persists its next continuation, cutting a long response off mid-sentence.
With the flag off, hosted apps use the existing client-driven `auto_continue`
re-POST path instead, which is slower to resume but cannot be killed by a
server-side timeout.

</Callout>

Only enable this once you've verified the hosted deployment's regular
function reliably completes self-dispatched continuations inside that
60-second wall. For long, multi-step turns, prefer
`AGENT_CHAT_DURABLE_BACKGROUND` above — its background function has a much
larger budget and is the recommended path.

## What's next

- [**Durable Resume**](/docs/durable-resume) — how an interrupted run resumes
  without repeating completed side effects (a separate, always-on protection).
- [**Deployment**](/docs/deployment) — how the app and its functions are built
  and deployed to the platform.
- [**A2A Protocol**](/docs/a2a-protocol) — `A2A_SECRET` and the HMAC-signed
  self-dispatch the durable worker reuses.
