---
title: "1.17 release notes"
canonical_url: "https://www.nutrient.io/guides/document-engine/release-notes/1-17/"
md_url: "https://www.nutrient.io/guides/document-engine/release-notes/1-17.md"
last_updated: "2026-08-03T00:00:00.000Z"
---

Before attempting to upgrade to Document Engine 1.17, make sure your application runs as expected on your current version. If you’re on version 1.6.1 or later, you can upgrade directly to 1.17. If you’re on an earlier version, follow the step-by-step upgrade path outlined in our [upgrade](https://www.nutrient.io/guides/document-engine/release-notes/upgrading.md#general-advice) guide.

## Highlights

This release adds remote URL fetch controls, asynchronous Build API processing, a cached full-text search index, and faster DOM printing for Nutrient Web SDK.

### Remote URL fetch controls

Customer-controlled remote URL fetches now use a central policy with safe defaults and explicit configuration for self-hosted deployments that need to fetch from known private or internal destinations.

### Asynchronous Build API processing

The Build API can now accept multipart build requests for background processing with `Prefer: respond-async`. Document Engine returns `202 Accepted` with status and result URLs; supports `Idempotency-Key` for safe retries; supports uploaded files, remote URL inputs, and Document Engine document inputs; and lets clients request an allowed processing queue.

Asynchronous results are retained for one day by default, while job metadata is retained for one week by default. For setup, limitations, and examples, see the asynchronous Build API processing section.

### Faster text search with a cached full-text index

Plain text search can now be served from a cached full-text search (FTS) index instead of rescanning the source PDF on every query, turning multisecond searches on large documents into subsecond ones. In our large-file benchmarks, whole-document search on a 5,000-page PDF went from several seconds to well under 200 ms. The feature is opt-in and the `/search` response shape is unchanged.

#### Known issue

In Document Engine 1.17.0, full-text search index build jobs are submitted to the `search_index` queue, but this queue isn’t enabled by default. If no Document Engine node processes `search_index`, index jobs remain queued and search requests fall back to the previous per-query scan.

At least one Document Engine node must include `search_index` in `ASYNC_WORKER_QUEUES`. A typical single-node configuration is:

`ASYNC_WORKER_QUEUES: "default=5,search_index=2"`

`ASYNC_WORKER_QUEUES` specifies the complete set of queues processed by that node:

- `search_index=2` enables processing of full-text search index jobs.

- `default=5` isn’t required for full-text search, but at least one node in the deployment must process `default` for regular asynchronous operations and asynchronous result and metadata cleanup.

In a split deployment, `default` and `search_index` can be processed by different nodes. Restarting a node after adding `search_index` allows existing queued index jobs to be processed; documents don’t need to be recreated.

This workaround is specific to Document Engine 1.17.0. In the next Document Engine release, full-text search index jobs will move to the `default` queue. After upgrading, remove `search_index` from `ASYNC_WORKER_QUEUES` and continue ensuring at least one node processes `default`.

### Faster DOM printing for Nutrient Web SDK

Document Engine now renders a document’s pages for printing in a single batched, streamed request instead of one request per page. This cuts print-preparation time for Nutrient Web SDK’s server-backed DOM printing (`PrintMode.DOM`), with the largest gains for high-quality prints and on higher-latency connections.

## Breaking changes

This release includes breaking changes that may require updates to existing deployments and integrations.

### Verified TLS connections now require SAN certificates

Document Engine now runs on Erlang/OTP versions that follow [RFC 9525](https://www.rfc-editor.org/rfc/rfc9525.html) hostname verification behavior and no longer fall back to the certificate subject Common Name when a certificate has no matching Subject Alternative Name entry. Erlang/OTP marks this as a potential incompatibility related to [CVE-2026-42790](https://www.cve.org/CVERecord?id=CVE-2026-42790) in the [SSL/Public Key changelog](https://github.com/erlang/otp/blob/OTP-29.0.2/lib/public_key/doc/notes.md#public_key-1211).

The most common startup-impact case is self-hosted deployments that connect Document Engine to PostgreSQL with `PGSSL=true` and certificate verification enabled. A PostgreSQL server certificate that only identifies the server with `CN=<host>` can fail hostname verification after the upgrade.

The same certificate requirement can also affect other verified TLS connections made by Document Engine, such as remote HTTPS downloads when certificate verification is configured, Redis TLS, signing service or timestamp authority URLs, and customer-managed HTTPS asset storage endpoints. Public cloud and Nutrient-hosted endpoints are expected to already use Subject Alternative Name certificates.

Regenerate affected server certificates with matching DNS or IP Subject Alternative Name entries — for example, `DNS:db.example.com` or `IP:10.0.0.5` — before upgrading.

`PGSSL_CERT_COMMON_NAME` can still configure the hostname Document Engine expects, but it doesn’t restore Common Name fallback. As a temporary workaround only, deployments can disable hostname verification with `PGSSL_DISABLE_HOSTNAME_VERIFY=true` or disable PostgreSQL certificate verification with `PGSSL_DISABLE_VERIFY=true`.

### Remote URL fetches now allow only public destinations by default

Customer-controlled remote URL fetches now use the `public_only` policy by default. This applies to document creation from a remote URL, Build API URL file inputs, remote document assets, and Office template image URLs.

With the default policy, Document Engine resolves DNS and rejects local, private, link-local, metadata-service, and other blocked address ranges. This can reject URLs that previously worked in self-hosted deployments, such as URLs pointing to RFC1918 intranet hosts.

If your deployment intentionally fetches from controlled internal destinations, configure the new remote URL fetch options before upgrading. Use `REMOTE_URL_FETCH_POLICY=allowlist_only` with `REMOTE_URL_FETCH_ALLOWED_HOSTS` or `REMOTE_URL_FETCH_ALLOWED_CIDRS` for the strictest configuration for non-critical destinations, or `REMOTE_URL_FETCH_POLICY=allow_private` when the deployment must allow RFC1918 IPv4 or IPv6 ULA destinations. Localhost, link-local, and cloud metadata addresses remain blocked unless `REMOTE_URL_FETCH_POLICY=allow_all` is used.

### Invalid type values on Instant JSON exports are rejected

The Instant JSON export endpoints (`GET /api/documents/:documentId/document.json` and the layer variant) now validate the `type` query parameter. A request with a `type` value other than `full` or `diff` is rejected with a `400` response. Such requests previously ignored the unknown parameter and returned the full export with a `200`. Requests without a `type` parameter are unaffected.

### Asynchronous job status responses use a unified shape

`GET /api/async/jobs/{jobId}` now returns the same response shape for all asynchronous jobs, including bulk document deletion and document asset migration jobs.

Previously, existing asynchronous jobs returned only `status` and optional `details`:

```json

{
  "data": {
    "status": "failed",
    "details": [
      {
        "description": "failed"
      }
    ]
  }
}

```

They now return job metadata and normalized error information:

```json

{
  "data": {
    "job_id": "01J...",
    "status": "failed",
    "created_at": "2026-05-26T10:00:00Z",
    "updated_at": "2026-05-26T10:00:00Z",
    "error": {
      "reason": "async_job_failed",
      "description": "failed"
    }
  }
}

```

Completed jobs now include the same metadata. Fields that don’t apply to the current job state are omitted:

```json

{
  "data": {
    "job_id": "01J...",
    "status": "completed",
    "created_at": "2026-05-26T10:00:00Z",
    "updated_at": "2026-05-26T10:00:00Z",
    "expires_at": "2026-05-28T10:00:00Z"
  }
}

```

Clients that read `data.details` from asynchronous job status responses should switch to `data.error.description`. Clients that need a machine-readable failure reason should use `data.error.reason`.

### Asynchronous job polling requires a job access token

Asynchronous job admission responses now include a signed `job_access_token`. Token values are prefixed with `jat_`. Clients must send this token in the `X-Async-Job-Token` header when calling `GET /api/async/jobs/{jobId}` or `GET /api/async/jobs/{jobId}/result`.

Asynchronous jobs created before this upgrade don’t have a `job_access_token`; wait for existing asynchronous jobs to finish before upgrading, or resubmit those operations after upgrade.

This applies to asynchronous Build API jobs, bulk document deletion jobs, and document asset migration jobs. The token is scoped to one job and is returned again when an asynchronous Build API request is replayed with the same `Idempotency-Key`.

Bulk document deletion and document asset migration responses keep their existing `jobId` field name for compatibility. Their new access token field uses `job_access_token`, matching asynchronous Build API admission responses.

## Known issue: Fresh deployments with S3 storage and role-based authentication

In Document Engine 1.16.0 and 1.17.0, first-time deployments against a new, empty database fail during database migrations when `ASSET_STORAGE_BACKEND=s3` is configured with role-based AWS authentication (for example, IAM Roles for Service Accounts or an EC2 instance role) — that is, when `ASSET_STORAGE_S3_ACCESS_KEY_ID` and `ASSET_STORAGE_S3_SECRET_ACCESS_KEY` aren’t set. The container crash-loops during startup with `ArgumentError: the table identifier does not refer to an existing ETS table` referring to `ExAws.Config.AuthCache`.

Existing installations aren’t affected: The failing migration only runs against a fresh database, so upgrades from earlier versions work normally.

To work around the issue, complete the initial database migrations with Document Engine 1.15.1 — the latest version that isn’t affected — and then upgrade:

1. Deploy `pspdfkit/document-engine:1.15.1` with your regular configuration and `ENABLE_DATABASE_MIGRATIONS=true`, and wait for it to start successfully.

2. Update the image back to your target version. The remaining migrations run on the next startup as part of the normal upgrade path.

This also works if you already attempted to start 1.16.0 or 1.17.0 against the new database: Migrations resume where they stopped. It’s a one-time step per database; later upgrades of the same installation need no special handling.

This will be resolved in the next Document Engine release.

## Deprecations

This release doesn’t include any deprecations.

## Asynchronous Build API processing preview

The Build API can now process build requests asynchronously when the request includes the `Prefer: respond-async` header:

```bash

curl -X POST "http://localhost:5000/api/build" \
  -H "Authorization: Token token=<secret>" \
  -H "Prefer: respond-async" \
  -H "Idempotency-Key: <unique-key>" \
  -F instructions='{"parts":[{"file":"document"}]}' \
  -F 'document=@document.pdf'

```

The initial response is `202 Accepted` and includes `Location`, `Preference-Applied`, a status URL, a result URL, a signed `job_access_token` prefixed with `jat_`, and the job expiry time. Uploaded multipart files are stored durably before the `202` response is returned.

Asynchronous Build supports uploaded file inputs, remote URL inputs, Document Engine document inputs, input PDF passwords in Build instructions, and output PDF password fields. Worker-only secrets — such as remote URLs, input PDF passwords, and output PDF passwords — are encrypted in asynchronous job metadata before they’re stored in the database. Configure `ASYNC_JOB_ENCRYPTION_KEYS` with a dedicated keyring before accepting asynchronous Build jobs that contain these values. If the keyring isn’t configured, Document Engine rejects those requests with `422 async_job_encryption_keys_required` before creating a job.

Document Engine document inputs are checked during asynchronous admission. The referenced document and layer must exist and be accessible to the authenticated caller before an asynchronous job is created. These inputs remain live sources: If the document or layer is deleted before processing, the job fails with `source_unavailable`; if the source document, selected layer, or source PDF changes after admission, the job fails with `source_changed`. Selected-layer changes include document records such as annotations and form data, even when the underlying source PDF asset is unchanged.

Asynchronous admission also supports `Idempotency-Key`. Keys are scoped by tenant, operation, method, and route. Reusing the same key with the same request payload, relevant headers, and queue selection returns the original job response, while reusing it with a different request returns `409 Conflict`. Keys can be reused after the asynchronous job metadata retention window expires. Replayed admissions return the same `job_access_token`.

The background worker restores the persisted request, runs the Build API operation, stores the generated output, and removes staged input files after the job reaches a terminal state. Clients can poll the status URL until the job reports `completed` or `failed`.

When a job completes, the status response includes result metadata and a `result_url`. Clients can download the result through:

```bash

curl -X GET "http://localhost:5000/api/async/jobs/<job_id>/result" \
  -H "Authorization: Token token=<secret>" \
  -H "X-Async-Job-Token: <job_access_token>" \
  --output result.pdf

```

The result endpoint returns:

- `200` with the result body when the job completed successfully.

- `404` with `async_job_not_found` when the job doesn’t exist; access is
  denied; or the job access token is missing, invalid, or expired.

- `404` with `async_result_unavailable` when the authorized job exists but no
  result asset is available.

- `409` when the job hasn’t completed or the job failed or was canceled.

- `410` when the result has expired.

The current preview rejects PDF password headers and document- or layer-scoped build requests before creating an asynchronous job. Put PDF passwords in the Build API instructions instead of `nutrient-pdf-password` or `pspdfkit-pdf-password` headers.

Clients can also request a processing queue using the `Prefer` parameter:

```http

Prefer: respond-async;queue=custom-queue

```

Queue names use the same validation as asynchronous job queue configuration: 1 to 127 characters, starting with a lowercase letter, followed by lowercase letters, digits, underscores, or hyphens. If no queue is provided, the default queue is used. Requested queues, including the default queue, must be listed in `ASYNC_ADMISSION_QUEUES`. The default admission queue is `default`; set `ASYNC_ADMISSION_QUEUES` to an empty value to disable asynchronous operation admission.

Asynchronous Build admission also requires persistent database storage. When persistent database storage isn’t available, Document Engine rejects `Prefer: respond-async` Build requests with `503 async_admission_disabled`.

Admitting work and processing work are configured separately. At least one Document Engine node must process the selected queue with `ASYNC_WORKER_QUEUES` — for example, `ASYNC_WORKER_QUEUES=default=1`. Single-node deployments process the `default` queue by default. If no node processes the selected queue, an accepted job remains `not_started`. Worker-only asynchronous nodes should set `CLUSTERING_ENABLED=false`; otherwise, clustered document routing can forward document-scoped customer requests to them.

For complete setup guidance, response schemas, polling examples, result-download behavior, and troubleshooting status codes, see the [asynchronous jobs API reference](https://www.nutrient.io/api/reference/document-engine/upstream/#tag/Async-Jobs).

## Asynchronous job observability

Document Engine now emits asynchronous job-specific metrics and structured logs for background job lifecycle events. Operators can monitor queued, running, completed, failed, retried, expired, and missing-worker asynchronous jobs without relying only on generic Oban telemetry.

The new metrics include asynchronous job queue time, execution duration, status transitions, retry and failure counters, missing-worker counters, and current queue-state gauges grouped by queue, worker, and Oban state. Metric labels are bounded to operational dimensions such as queue, worker, type, state, previous status, and error kind; asynchronous job IDs and Oban job IDs are available in structured logs instead of metric labels.

Queue-state gauges are reported by the Oban leader so clustered deployments don’t multiply the same database-backed counts by the number of nodes. Non-leader nodes clear previously emitted queue-state groups to avoid stale gauge series during leadership changes.

## Configuration options

This release adds and updates environment variables for controlling asynchronous job behavior.

### Asynchronous job processing

Document Engine now centralizes asynchronous job configuration and documents the operator-facing environment variables used by background processing.

`ASYNC_JOBS_TTL` keeps its existing name, but it now describes asynchronous job result retention more precisely: The value is applied after an asynchronous job reaches a terminal state, such as completed, failed, or canceled. The default is now one day.

This release adds the following asynchronous job configuration options:

- `ASYNC_JOB_TIMEOUT` for the maximum asynchronous job processing time, in seconds.

- `ASYNC_JOB_MAX_RETRIES` for the maximum number of retries after the initial asynchronous job execution attempt fails. The default is two retries.

- `ASYNC_JOB_RETRY_BACKOFF_SECONDS` for the base retry backoff used when failed asynchronous jobs are scheduled again. The default is 15 seconds.

- `ASYNC_JOBS_METADATA_TTL` for the amount of time asynchronous job metadata is retained. The default is one week. It must be greater than or equal to `ASYNC_JOBS_TTL`.

- `ASYNC_ADMISSION_QUEUES` for selecting which background job queues can be used when admitting new asynchronous operations. The default is `default`. Set it to an empty value to disable asynchronous operation admission.

- `ASYNC_WORKER_QUEUES` for selecting which background job queues a node processes and with what concurrency. The default is `default=5`. Set it to an empty value to make the node skip asynchronous background processing.

- `ASYNC_ADMISSION_QUEUE_LIMITS` for optional per-queue admission limits. Unset queues are unlimited. By default, the `default` queue is unlimited. Limits are best-effort admission backpressure, not strict concurrency ceilings.

- `ASYNC_JOB_ENCRYPTION_KEYS` for encrypting asynchronous job worker metadata, such as remote URLs and PDF passwords, with a dedicated keyring.

These options are intended for deployments that run separate API and worker nodes, or that need to tune background processing separately from request handling. Asynchronous operations still require persistent database storage for job metadata. When persistent database storage isn’t available, asynchronous Build admission returns `503 async_admission_disabled`. Result-producing operations require a configured asset storage backend for output assets. The full syntax is documented in the configuration reference.

`ASYNC_JOB_ENCRYPTION_KEYS` accepts comma-separated `key_id:base64_32_byte_key[:current]` entries. Exactly one entry must be marked `:current`; older keys must stay configured until all jobs encrypted with them have expired. Asynchronous Build requests that contain remote URLs, input PDF passwords, or output PDF passwords require this keyring. If the keyring isn’t configured, Document Engine rejects those requests with `422 async_job_encryption_keys_required` before creating a job. If an old asynchronous encryption key is removed too early, affected jobs can fail because worker metadata cannot be decrypted. Restore the missing key and retry the worker before the final asynchronous job retry is exhausted.

At least one node must process every queue that receives jobs. Existing asynchronous endpoints such as bulk document deletion and document asset migration submit work to the `default` queue. Single-node deployments do this by default. Split deployments that disable worker queues on API nodes need at least one worker node configured with, for example, `ASYNC_WORKER_QUEUES=default=1`. If no node processes the queue, jobs can be accepted but remain `not_started`. Asynchronous result and metadata cleanup also runs on the `default` queue.

Worker-only asynchronous nodes should also set `CLUSTERING_ENABLED=false`. Document Engine clustering can route document-scoped customer requests to any routable cluster member, so a node that only exists to process background queues must stay out of the clustering ring unless it’s also intended to serve document requests.

## Diff export type for document.json

The Instant JSON export endpoints (`GET /api/documents/:documentId/document.json` and `GET /api/documents/:documentId/layers/:layerName/document.json`) now accept an optional `type` query parameter.

The default, `type=full`, keeps the existing behavior: The export includes all form fields and widget annotations, including unmodified ones that originate from the uploaded PDF.

With `type=diff`, form fields and widget annotations that originate from the uploaded PDF and were never modified through Document Engine are excluded, so the exported form fields only describe changes made on top of the uploaded PDF: user-created form fields and modified base-PDF form fields. Use this when applying the export to a copy of the original PDF (for example, via `apply_instant_json`), where the unmodified form fields are already part of the PDF.

The `type` parameter only affects form fields and their widget annotations. Other record types (annotations, bookmarks, comments) and form field values are included in both export types. Copying a document resets the copy’s modification tracking, so changes made before the copy are treated as part of the copy’s baseline.

An invalid `type` value is rejected with a `400` response.

## HTTP header limit configuration

Document Engine now documents `MAX_HEADER_VALUE_LENGTH` and adds `MAX_HEADERS` for deployments that run behind trusted reverse proxies or load balancers that legitimately add large or numerous HTTP request headers.

`MAX_HEADER_VALUE_LENGTH` controls the maximum accepted size of an individual request header value and defaults to `8192` bytes. `MAX_HEADERS` controls the maximum accepted number of request headers and defaults to `100`.

## Preferred annotation fonts

Document Engine now supports `PSPDFKIT_PREFERRED_FONTS`, a comma-separated list of font names or family names to try before the general annotation font fallback algorithm.

Document Engine first tries the font requested by the annotation. If that font is unavailable, or if it cannot render the annotation text, Document Engine tries the configured preferred fonts in order. If none of the preferred fonts can render the text, Document Engine continues with the default fallback behavior.

This is useful for workflows where Web SDK standalone is configured with custom fonts and the resulting annotation JSON is later processed by Document Engine. Configure the same fonts in Document Engine and include them in `PSPDFKIT_PREFERRED_FONTS` to keep server-side annotation output closer to the standalone rendering.

## Remote URL fetch controls

Document Engine now applies a central policy to customer-controlled remote URL fetches, such as document creation from a remote URL, Build API URL file inputs, remote document assets, and Office template image URLs.

By default, `REMOTE_URL_FETCH_POLICY` is `public_only`. In this mode, Document Engine resolves DNS; rejects local, private, link-local, metadata-service, and other blocked address ranges; and connects to the resolved address while preserving the original `Host` header and TLS SNI.

Self-hosted deployments that intentionally fetch from controlled internal destinations can configure the policy explicitly:

- `REMOTE_URL_FETCH_ALLOWED_HOSTS` accepts comma-separated hostnames or wildcard DNS patterns, such as `assets.example.com` or `*.assets.example.com`.

- `REMOTE_URL_FETCH_ALLOWED_CIDRS` accepts comma-separated CIDR ranges.

- `REMOTE_URL_FETCH_POLICY` accepts `allowlist_only`, `public_only`, `allow_private`, or `allow_all`.

- `REMOTE_URL_FETCH_ALLOW_EMBEDDED_CREDENTIALS` controls whether URLs such as `https://user:pass@example.com/file.pdf` are accepted. It’s disabled by default.

Use `allowlist_only` to deny all remote destinations except configured hosts or CIDRs while still keeping critical SSRF ranges blocked. Use `allow_private` only when a deployment must fetch from RFC1918 IPv4 or IPv6 ULA destinations; localhost, link-local, and cloud metadata addresses remain blocked. `allow_all` is intended only as an emergency escape hatch for controlled self-hosted deployments.

## Faster text search with a cached full-text index

Document Engine can now answer plain text search requests from a cached full-text search (FTS) index instead of rescanning the source PDF on every query. For large documents, this turns multisecond searches into ones that complete in well under a second.

Previously, every `/search` request reopened the source PDF and scanned the requested pages from scratch. That cost grows with document size, so search on large blueprint- or manual-style PDFs could take several seconds per query. The new path builds an FTS index once per source PDF, persists it, and reuses it across requests and nodes.

The search response shape is unchanged, so SDK clients need no changes.

### How it works

- When enabled, an eligible text search resolves the document’s current
  source PDF, fetches the cached FTS index (building it in the
  background if it’s missing), queries the index, and returns results in
  the existing `/search` JSON shape.

- The index is content-addressed by the source PDF, so two documents
  that share the same source PDF share one index, and replacing a
  document’s source PDF naturally invalidates its index.

- Indexes are built by background jobs and persisted to your configured
  asset storage (S3, Azure Blob Storage, or PostgreSQL). A node restart
  or a request routed to a different node reuses the persisted index
  rather than rebuilding it.

- Newly persisted indexes are also recorded in the database with their
  source PDF hash, index format version, and asset storage backend. This
  metadata is internal and prepares the stored artifacts for future
  cleanup.

- The index build never blocks upload, source-PDF replacement, or
  document deletion. If a build fails or an index is missing, the request
  transparently falls back to the previous per-query scan.

### Known limitation: Persisted indexes aren’t yet reclaimed

Persisted index artifacts aren’t garbage-collected in this release. When a document is deleted or its source PDF is replaced, the old index left in asset storage isn’t removed automatically, so index storage grows over time. Newly created index artifacts are tracked in the database so they can be found and removed by a future cleanup job, but that cleanup job isn’t part of this release.

Because indexes are shared by content (every document with the same source PDF shares one index), safe reclamation requires reference counting; that reaper is planned as a follow-up. The artifacts are small relative to the source PDFs, and local on-disk caches are still evicted under their normal size cap — only the asset-storage copies accumulate.

### What uses the new path

To keep behavior safe and predictable, FTS only handles plain text search. These requests continue to use the previous search path:

- Searches that include annotations (`include_annotations=true`).

- Regex and preset searches.

- Case-sensitive searches (`case_sensitive=true`).

- Any document without a built or reachable index (a build is enqueued
  and the request falls back for now).

- Very broad queries that reach the current FTS result cap. These fall
  back to the previous scan so page-window semantics remain correct.

The `start` and `limit` page-window semantics are preserved exactly: as before, they select the range of pages to search, not a result offset.

### Enabling it

This feature is opt-in and disabled by default; deploying this release doesn’t change search behavior until you enable it. It’s controlled by environment variables:

- `SEARCH_FTS_ENABLED` (default `false`) — Route eligible text searches
  through the FTS index.

- `SEARCH_FTS_BUILD_ON_WRITE` (default `false`) — Build the index in the
  background when a document is created or its source PDF is replaced.

- `SEARCH_FTS_ENRICHMENT` (default `daemon`) — How matches are turned
  into preview text and highlight rects. `daemon` is the supported value
  for this release.

Built indexes are always persisted to your configured asset storage so they survive restarts and node hops; this happens automatically once an index is built.

A typical rollout enables build-on-write first so indexes are available. Then it enables `SEARCH_FTS_ENABLED` for the query path.

### Measured improvements

Internal benchmark runs in a development environment showed substantial latency reductions for large-document text searches when the request was served by the cached FTS index. The exact improvement depends on hardware, concurrent load, document characteristics, query term frequency, and whether the query can complete on the FTS path without falling back.

For example, a whole-document text search on a large, content-heavy document dropped from roughly 4 seconds to around 220 milliseconds when served by the cached index. This is a single data point, not a guaranteed result: Other documents, queries, and deployments will see different figures.

The improvement is most visible on large documents where a repeated scan is expensive. For small documents, where a scan is already fast, the two paths perform similarly.

## Database migrations

This release includes database migrations that add asynchronous operation metadata columns to the existing `async_jobs` table:

- `request`

- `result`

- `error`

- `owner`

- `metadata_expires_at`

Existing asynchronous job rows remain readable during rolling deploys, but the public status response shape changes as described in the breaking changes notes.

Separate concurrent-index migrations add asynchronous job indexes used by cleanup and admission backpressure:

- `async_jobs_reaper_idx`

- `async_jobs_metadata_reaper_idx`

- `async_jobs_active_queue_idx`

This release also adds a private `async_job_admissions` table used while admitting asynchronous operation requests. The table reserves a job ID, tracks durably staged input blobs, and stores idempotency reservation metadata until the public `async_jobs` row can be committed. It’s internal implementation state and isn’t part of the public asynchronous job API.

This release also adds a private `search_index_blobs` table used to track persisted full-text search index artifacts. Search index bytes continue to live in the configured asset storage backend, but this table records the stored artifact hash, source PDF hash, search index format version, and backend metadata needed to identify those artifacts later.

The migration creates:

- `search_index_blobs`

- `search_index_blobs_source_pdf_sha256_index`

- `search_index_blobs_sha256_backend_info_index`

The table is internal implementation state and doesn’t change the public search API. It doesn’t delete existing or newly created search index artifacts by itself; it only records newly persisted artifacts so future cleanup can find them.
---

## Related pages

- [1 1](/guides/document-engine/release-notes/1-1.md)
- [1 10](/guides/document-engine/release-notes/1-10.md)
- [1 11](/guides/document-engine/release-notes/1-11.md)
- [1 12](/guides/document-engine/release-notes/1-12.md)
- [1 13](/guides/document-engine/release-notes/1-13.md)
- [1 14](/guides/document-engine/release-notes/1-14.md)
- [1 15](/guides/document-engine/release-notes/1-15.md)
- [1 16](/guides/document-engine/release-notes/1-16.md)
- [1 2](/guides/document-engine/release-notes/1-2.md)
- [1 3](/guides/document-engine/release-notes/1-3.md)
- [1 4](/guides/document-engine/release-notes/1-4.md)
- [1 5](/guides/document-engine/release-notes/1-5.md)
- [1 6](/guides/document-engine/release-notes/1-6.md)
- [1 7](/guides/document-engine/release-notes/1-7.md)
- [1 8](/guides/document-engine/release-notes/1-8.md)
- [1 9](/guides/document-engine/release-notes/1-9.md)
- [Upgrading from PSPDFKit Processor](/guides/document-engine/release-notes/upgrading-from-processor.md)
- [Upgrading from PSPDFKit Server](/guides/document-engine/release-notes/upgrading-from-server.md)
- [Upgrading Document Engine](/guides/document-engine/release-notes/upgrading.md)

