---
title: "DeferredBatchV3"
description: "Deferred (background) batch for Bitrix24 REST API version 3: send thousands of commands in one call, follow the job status, and get the decoded rows — in one call with make(), or step by step."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/deferred-batch-rest-api-ver3"
last_updated: "2026-10-01"
---
# DeferredBatchV3

> Deferred (background) batch for Bitrix24 REST API version 3: send thousands of commands in one call, follow the job status, and get the decoded rows — in one call with make(), or step by step.

## Overview

`$b24.actions.v3.deferredBatch` wraps the portal's `rest.deferredbatch.*`
methods. A deferred batch is a **background job**:

1. `rest.deferredbatch.add` takes **all** commands in one call — thousands of
them, far past the 50 of a synchronous [`batch`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-rest-api-ver3.md).
Measured: 5000 `tasks.task.get` commands in one job, finished in under 5 seconds
([#570](https://github.com/bitrix24/b24jssdk/issues/570#issuecomment-5844466488){rel="[\"nofollow\"]"}).
2. The portal runs them in the background. The job's status goes
`pending` → `processing` → `done`, or ends in `error` (all four observed).
3. The results are stored as a gzip-compressed JSON file. The SDK downloads it
and decodes it into rows — one row per command, in command order.

Compared with [`batchByChunk`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-by-chunk-rest-api-ver3.md),
which sends one request per 50 commands, a deferred batch is one request to
start, then status checks until it is done. How the portal counts a background
job against the operating-time limit has not been measured.

> [!WARNING]
> Deferred batches depend on the portal's **plan**. On a plan without them every
> call answers `FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN` (observed on such a portal,
> [#570](https://github.com/bitrix24/b24jssdk/issues/570){rel="[\"nofollow\"]"}). Which scope the methods
> need has not been measured.

> [!WARNING]
> **Rules measured on the portal** ([#570](https://github.com/bitrix24/b24jssdk/issues/570#issuecomment-5844638165){rel="[\"nofollow\"]"}):
> - **v3 methods only.** A v2 method (`user.current`) or an unknown one is refused
> at `add` with `INVALID_METHOD`; no job is created.
> - **All or nothing.** If one command fails — a `get` of a record that does not
> exist is enough — the whole job ends in `error` (`errorMessage: "Result does not exists"`) and there is **no result file**, not even for the commands that
> worked. Build commands that cannot fail on data: list pages rather than a
> `get` per id.
> - **Duration depends on the method, not only the count.** 5000 cheap `get`s
> finished in seconds; 500 `rest.scope.list` were still running after 15
> minutes. Set `timeout` for your workload.
> - **A running job cannot be deleted** — the portal answers `BATCH_PROCESSING`.
> Delete it once it is `done` or `error`.

> [!NOTE]
> **Measured through a webhook.** In a browser (`B24Frame`) the result file can
> be read: the portal serves it with `Access-Control-Allow-Origin: *` (measured, [#570](https://github.com/bitrix24/b24jssdk/issues/570#issuecomment-5844778591){rel="[\"nofollow\"]"});
> a full run from a frame
> has not been done. `idempotencyKey` is not available in a browser — see
> [Idempotency-Key](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/call-rest-api-ver3.md#idempotency-key).

There are two ways to use it:

| You want to… | Use |
| --- | --- |
| Run the commands and get the rows, with a progress callback | [`make()`](#method-signature) |
| Start a job now and collect it later — another request, a queue worker, your own UI polling | [the single steps](#step-by-step) |

## Method Signature

```ts-type
make<T = unknown>(options: ActionDeferredBatchV3): Promise<Result<T[]>>

add(options: ActionDeferredBatchAddV3): Promise<Result<DeferredBatchJob>>
get(id: number): Promise<Result<DeferredBatchJob>>
waitFor(id: number, options?: ActionDeferredBatchWaitV3): Promise<Result<DeferredBatchJob>>
download<T = unknown>(id: number): Promise<Result<T[]>>
delete(id: number): Promise<Result<boolean>>
list(): Promise<Result<DeferredBatchJob[]>>
getDownloadUrl(id: number): Promise<Result<string>>
decode(bytes: ArrayBuffer | Uint8Array): Promise<unknown[]>
```

### All in one: `make()`

```ts
// const $b24 = ...
import type { BatchCommandsArrayUniversal, DeferredBatchJob } from '@bitrix24/b24jssdk'

interface TaskPage { items: Array<{ id: number, title: string }> }

// 200 pages of 50 tasks. List pages, not a `get` per id: one failing command
// fails the whole job, while a page past the end is just empty (measured).
const calls: BatchCommandsArrayUniversal = Array.from({ length: 200 }, (_, i) => [
  'tasks.task.list',
  { select: ['id', 'title'], order: { id: 'ASC' }, pagination: { page: i + 1, limit: 50 } }
])

const response = await $b24.actions.v3.deferredBatch.make<TaskPage>({
  calls,
  // Called on every status change, e.g. pending → done.
  onStatus: (job: DeferredBatchJob) => console.log(`job #${job.id}: ${job.status}`)
})

if (!response.isSuccess) {
  throw new Error(response.getErrorMessages().join('; '))
}

const pages = response.getData()! // one row per command, in order
const titles = pages.flatMap(page => page.items.map(task => task.title))
console.log(`${titles.length} tasks in ${pages.length} pages`)
```

`make()` adds the job, waits for it, downloads and decodes the result file, and
deletes the job. What the portal answers never makes it throw: check
`isSuccess`. It throws only for malformed `calls`, before anything is sent.

#### Options

| Option | Type | Default | Meaning |
| --- | --- | --- | --- |
| `calls` | `DeferredBatchCalls`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | — | The commands: `[['method', params], …]`, `[{ method, params }, …]`, or both mixed. Named commands are not supported — the result is an array. |
| `onStatus` | `(job: DeferredBatchJob) => void`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | — | Called when the status changes, the first status included. An exception in it — or a rejection, if it is `async` — is logged and ignored. |
| `pollInterval` | `number`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | `2000` | Milliseconds between status checks. At least `250`. |
| `timeout` | `number`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | `600000` | Milliseconds to wait for `done` / `error`. |
| `signal` | `AbortSignal`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | — | Stops waiting. |
| `deleteAfter` | `boolean`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | `true` | Delete the job and its file after reading. |
| `idempotencyKey` | `string`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | — | Sent with `add`. Use one key per run: by the portal's idempotency contract — not measured for deferred batches — a retry of an interrupted `make()` with the same key then does not start a second job, while a key reused after a finished `make()` deleted its job would replay the id of a job that no longer exists. See [Idempotency-Key](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/call-rest-api-ver3.md#idempotency-key). |
| `requestId` | `string`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} | — | Sent as `bx24_request_id` with `add`. |

## Examples

### Progress in a UI, and cancelling

```ts
// const $b24 = ...
declare const calls: [string, Record<string, unknown>][]
declare function showStatus(text: string): void

const controller = new AbortController()
// e.g. wired to a "Cancel" button: cancelButton.onclick = () => controller.abort()

const response = await $b24.actions.v3.deferredBatch.make({
  calls,
  pollInterval: 3_000,
  timeout: 30 * 60_000,
  signal: controller.signal,
  onStatus: job => showStatus(job.status === 'done' ? 'Downloading results…' : `Status: ${job.status}`)
})

if (!response.isSuccess) {
  // JSSDK_DEFERRED_BATCH_ABORTED, _TIMEOUT or _FAILED — see "Errors" below.
  showStatus(response.getErrorMessages().join('; '))
}
```

Aborting or timing out stops **waiting**, not the job: it keeps running on the
portal, and can be collected later with the steps below. `make()`'s `Result`
does not carry the job id; `onStatus` does — it is called with the job as soon
as it is first read — so keep the id from there if you may want to collect it.

### Step by step

Each step is one method and returns a `Result`:

| Method | Portal method | Returns |
| --- | --- | --- |
| `add({ calls, idempotencyKey?, requestId? })` | `rest.deferredbatch.add` | `Result<DeferredBatchJob>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} — the new job, usually `pending` |
| `get(id)` | `rest.deferredbatch.get` | `Result<DeferredBatchJob>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} |
| `waitFor(id, { pollInterval?, timeout?, signal?, onStatus? })` | `rest.deferredbatch.get`, repeated | `Result<DeferredBatchJob>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} — the job in its final state |
| `download<T>(id)` | `rest.deferredbatch.downloadresult` + the file | `Result<T[]>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} — the decoded rows |
| `delete(id)` | `rest.deferredbatch.delete` | `Result<boolean>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} — refused while the job is `processing` |
| `list()` | `rest.deferredbatch.list` | `Result<DeferredBatchJob[]>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} |
| `getDownloadUrl(id)` | `rest.deferredbatch.downloadresult` | `Result<string>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} — the file link (a credential, see below) |
| `decode(bytes)` | — | `Promise<unknown[]>`{className="language-ts-type shiki shiki-themes material-theme-lighter material-theme material-theme-palenight" language="ts-type" style=""} — gunzip + JSON of file bytes |

#### Start now, collect later

```ts
// const $b24 = ...
declare const calls: [string, Record<string, unknown>][]
declare function saveJobId(id: number): Promise<void>
declare function loadJobId(): Promise<number>

// Request 1: start the job and remember its id.
const added = await $b24.actions.v3.deferredBatch.add({ calls })
if (!added.isSuccess) {
  throw new Error(added.getErrorMessages().join('; '))
}
await saveJobId(added.getData()!.id)

// Request 2, later: is it ready?
const jobId = await loadJobId()
const job = (await $b24.actions.v3.deferredBatch.get(jobId)).getData()

if (job?.status === 'done') {
  const rows = await $b24.actions.v3.deferredBatch.download(jobId)
  console.log(`${rows.getData()?.length ?? 0} rows`)
  await $b24.actions.v3.deferredBatch.delete(jobId)
} else if (job?.status === 'error') {
  console.error(job.errorMessage)
}
```

#### The job object

```ts-type
interface DeferredBatchJob {
  id: number
  status: 'pending' | 'processing' | 'done' | 'error'
  commands?: Array<{ method: string, query: Record<string, unknown> }>
  createdAt?: string
  updatedAt?: string
  resultFileId?: number | null // set once `done`
  errorMessage?: string | null // set when `error`
}
```

## The result rows

A row is what that command returned, in command order — the same shape as one
entry of a synchronous v3 `batch` result: `{ item }` for a `get`, `{ items }` for
a `list`, and so on. The row carries no `time` block. There are no error rows:
a job with a failing command has no result file at all (see the rules above).

The whole file is held in memory while it is decoded. Rows that each carry a
full list page add up quickly; split very large exports into several jobs.

`download()` / `make()` decode the file for you. If you downloaded it some other
way, `decode()` does the same: it gunzips the bytes (the file is
`application/gzip`) and parses the JSON array. Bytes already inflated — a proxy
that decompressed them — are parsed as they are. Decoding uses the standard
`DecompressionStream`, available in Node.js 18+ and current browsers.

## Security: the download link is a credential

`rest.deferredbatch.downloadresult` answers with an absolute URL that carries
the caller's credential — measured: the webhook secret in the path; per the
portal's code, an access token for an OAuth app. The SDK downloads it itself,
keeps it out of the errors it builds, refuses redirects with it, and its log of
the `downloadresult` answer masks the secret and the token. `getDownloadUrl()` hands it to you: treat it like
the webhook URL itself. Deleting the job makes the link stop working.

## Error Handling

| Code | When |
| --- | --- |
| `JSSDK_DEFERRED_BATCH_FAILED` | The job ended with status `error` — one failing command is enough. `make()` logs the portal's `errorMessage` and deletes the job (unless `deleteAfter: false`); `waitFor()` returns the job as data, with its `errorMessage`. The message names the job id. |
| `JSSDK_DEFERRED_BATCH_TIMEOUT` | No final status within `timeout`. The job keeps running and cannot be deleted yet; the message names its id, to collect it later. |
| `JSSDK_DEFERRED_BATCH_ABORTED` | `signal` was aborted. A job already added keeps running; the message names its id. |
| `JSSDK_DEFERRED_BATCH_DOWNLOAD_FAILED` | The result file could not be downloaded; the message names the HTTP status. |
| `JSSDK_DEFERRED_BATCH_DECODE_FAILED` | The file is not valid gzip, or not a JSON array. |
| `JSSDK_DEFERRED_BATCH_GZIP_UNSUPPORTED` | The runtime has no `DecompressionStream`. |
| `JSSDK_DEFERRED_BATCH_EMPTY` | `calls` is empty — thrown, before anything is sent. |
| `JSSDK_DEFERRED_BATCH_UNEXPECTED_RESPONSE` | The portal answered without the data the step expects (e.g. `add` without an `item`). |
| `JSSDK_INTERACTION_BATCH_ROW_FAIL` | A command in `calls` is neither a tuple nor a `{ method, params }` object — thrown, before anything is sent. |
| `FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN` | The portal's plan has no deferred batches. |
| `INVALID_METHOD` | `add` named a v2 or unknown method; a deferred batch takes v3 methods only. No job was created. |
| `BATCH_PROCESSING` | `delete` of a job that is still `processing`. |

## Alternatives and Recommendations

- **Up to 50 commands, result needed right away:** a synchronous
[`batch`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-rest-api-ver3.md) is simpler and
returns in the same request.
- **More than 50, and the plan has no deferred batches:**[`batchByChunk`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-by-chunk-rest-api-ver3.md)
does the same work as one request per 50 commands.
- **Reading a whole list:** [`fetchList`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/fetch-list-rest-api-ver3.md)
or [`fetchTail`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/fetch-tail-rest-api-ver3.md)
page through it without building thousands of `get` commands.
- **Writes that must not repeat:** pass `idempotencyKey`, one per run, so a
retried `add` does not start a second job (the portal's idempotency contract;
not measured for deferred batches).
- **Cancelling:** `signal` works as on the list actions, but an abort comes
back as `JSSDK_DEFERRED_BATCH_ABORTED` on the `Result` rather than a thrown
`JSSDK_ACTION_ABORTED`, in line with `make()` never throwing.
- **Large result files:** the download uses the HTTP client's timeout (30 s by
default), not `timeout`. Raise `getHttpClient(ApiVersion.v3).ajaxClient.defaults.timeout`
for very large jobs.
- **Writes:** the job is all-or-nothing only for its *result*. A failing
command ends it in `error` without a file, but nothing says the writes that
ran before it were undone — so for writes, prefer commands that cannot fail,
and check the portal's data after an `error`.

## Sitemap

See the full [sitemap](/b24jssdk/sitemap.md) for all pages.
