Dedicated Engines

A dedicated engine is a private Meilisearch, Typesense, Elasticsearch or Visor server for your organization. It has its own HTTPS endpoint. You can use it two ways: through the Headband REST API like any other index, or directly with the engine's official client.

Dedicated engines are rolling out gradually. If your organization does not have them yet, the dashboard has no Engines page and the instance API answers 404.

Create an Engine

Create an engine from the dashboard, the CLI or the REST API.

In the dashboard

  1. Open Engines in the sidebar.
  2. Click Create engine.
  3. Pick the engine, lane, size and region, then click Create engine.

Open the engine to find its Connect card. It shows the endpoint, plus Node and curl code for that engine's client.

With the CLI

The engine commands need @headband/cli 0.2.0 or later.

terminal
npm install -g @headband/cli
headband login
headband instances create meilisearch --lane serverless --memory 1g
headband instances list
headband instances connect <id>

instances connect prints the endpoint and code for the engine's own client. instances create also takes --name, --region and --idle-timeout <seconds>.

With the REST API

POST/v1/instances

Create a dedicated engine. Only engine is required.

Needs an org-wide admin key. A project-scoped key answers 403 org_key_required.

Auth admin key onlyReturns 201
Request body
{
  "engine": "meilisearch",   // meilisearch, typesense, elasticsearch or visor
  "lane": "serverless",      // optional: serverless or always_on
  "memoryMb": 1024,          // optional: one of the sizes below
  "region": "ewr1",          // optional
  "idleTimeoutS": 300,       // optional: serverless idle timeout
  "name": "catalog"          // optional
}
Response
{
  "data": {
    "id": "<id>",
    "name": "catalog",
    "engine": "meilisearch",
    "engineVersion": "<version>",
    "lane": "serverless",
    "memoryMb": 1024,
    "region": "ewr1",
    "state": "provisioning",
    "unavailableReason": null,
    "endpoint": "https://<id>.ewr1.search.headband.dev",
    "idleTimeoutS": 300,
    "projectId": "<project id>",
    "branchId": "<branch id>",
    "isDefault": false,
    "pendingChanges": false,
    "changeRejected": false,
    "lastBackupAt": null,
    "bucketHome": false,
    "restartable": false,
    "lastWakeAt": null,
    "createdAt": "2026-10-05T12:00:00.000Z",
    "updatedAt": "2026-10-05T12:00:00.000Z"
  }
}

Wait until state is running before you use the engine. Poll GET /v1/instances/<id> or run headband instances show <id>. In our tests Meilisearch was ready within 2 seconds, Typesense within 5, and Elasticsearch took about 70.

state is unavailable while the server hosting the engine cannot serve it: it is down, or Headband's checks cannot reach it. unavailableReason then says which, as host_down or host_unreachable, and is null otherwise. Requests answer 503 instance_unavailable with Retry-After: 30 until the engine serves again. An engine that keeps its data in bucket storage moves to the other server by itself in about 2 to 3 minutes. An always-on Meilisearch or Elasticsearch engine on bucket storage and a paid plan normally also has a live read-only copy on the other server, which answers its searches and multi-searches in the meantime. Its writes and other requests still get the 503. See Errors and Service levels.

lastBackupAt is when Headband last backed up the engine's data. bucketHome is true for a Visor engine that keeps its data in object storage: Visor writes every acknowledged change there itself, so the engine takes no backups and lastBackupAt stays null. restartable is true when the engine's host can restart it. The engine page in the dashboard shows a Restart button only then.

Where Visor runs

Visor is offered in a region where a dedicated engine host runs it, and the dashboard lists it there. In a region where none does, creating a Visor engine answers 503 engine_not_available with the message Visor is not available in ewr1 yet. (naming the region). Nothing is created, so retry later or pick another engine.

Lanes, Sizes and Regions

The lane decides what an engine does when nobody uses it. A serverless engine parks after its idle timeout and wakes on the next request. An always-on engine keeps running.

EngineLanesSizes (memory)Default
MeilisearchServerless, always on512 MB, 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GBServerless, 512 MB
ElasticsearchAlways on1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GBAlways on, 1 GB
TypesenseAlways on; serverless in early access512 MB, 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GBAlways on, 512 MB
VisorServerless, always on512 MB, 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GBServerless, 512 MB

In the API, lane is serverless or always_on, and memoryMb is the size in megabytes, such as 1024 for 1 GB. The serverless idle timeout defaults to 300 seconds. It can be 60 seconds to 24 hours.

The default region is ewr1. A region with no engine hosts answers 400 unknown_region. You can change the lane, size and idle timeout later. The engine type and region are fixed.

What More RAM Buys You

We measured each engine at the sizes we sell, on our own production hardware, on 2026-10-29. The table shows searches a second with 4 clients, and p95 latency. Visor was not measured.

EngineSize1M docs: searches a secondp95100K docs: searches a secondp95
Meilisearch512 MB32082 ms
Meilisearch1 GBDoes not fit2,5621.9 ms
Meilisearch2 GB14372 ms4,3021.4 ms
Meilisearch4 GB30828 ms
Meilisearch8 GB1,7996.2 ms
Typesense512 MB40059 ms
Typesense1 GBNot measured40659 ms
Typesense2 GB106151 ms79816 ms
Typesense4 GB110146 ms
Typesense8 GBNot measured
Elasticsearch1 GBNot measured28367 ms
Elasticsearch2 GB12268 ms7829.3 ms
Elasticsearch4 GB27327 ms
Elasticsearch8 GB26827 ms

Meilisearch. RAM is what makes Meilisearch fast: it reads its index from disk through memory. At 1M documents, 8 GB served 12 times the searches of 2 GB, and almost 6 times those of 4 GB with the same 6 CPUs. 1M documents do not fit in 1 GB, because the index needs about 9 GiB of disk and the 1 GB size allows 8 GiB.

Typesense. More RAM does not make Typesense faster once its index fits. At 1M documents, 2 GB and 4 GB served about the same 106 to 110 searches a second. Its speed follows CPU: at 100K, 2 GB has 4 CPUs to 1 GB's 2 and served twice as many searches.

Elasticsearch. At 1M documents, going from 2 GB to 4 GB more than doubled Elasticsearch's searches a second and cut p95 from 68 to 27 ms. 8 GB gives the same speed as 4 GB: both have 6 CPUs, and both served about 270 searches a second. Pick 8 GB only if your index needs the memory. At 100K, the gain from 1 GB to 2 GB came with the extra CPUs.

How we measured:

  • Each size also gets CPU: 2 CPUs per started GB, at most 6. So 512 MB and 1 GB get 2 CPUs, 2 GB gets 4, and 4 GB and every larger size get 6. A bigger size can be faster because of memory, CPUs, or both, and from 4 GB up only memory grows.
  • We measured 4 GB and 8 GB at 1M documents again after the cap of 6 CPUs came in, on new engines created at that size. Those are the numbers shown. The smaller sizes kept their CPUs, so their numbers are from the first run.
  • Always-on engines on one of our production servers (AMD Ryzen 9 9950X), loaded through the engine's own API and searched through its public HTTPS endpoint and the Headband gateway. The client ran on the same server, so the numbers leave out your network.
  • Synthetic products: 100,000 or 1,000,000 documents of about 1 KB each, with a made-up 30,000-word vocabulary where a few words are very common. Real catalogs usually drop such words, so this mix is harder than most stores.
  • The query mix: 60% one or two keywords, 20% a keyword with a brand or price filter, 10% a keyword with category and brand facets, 10% a keyword sorted by price. 20 results a page, and typo tolerance on.
  • Searches a second come from 4 clients sending 3,000 searches back to back after a warmup. p95 is from the same run. A size that came within 0.7 to 1.6 times 50 ms ran twice, and Meilisearch at 4 GB ran three times. The worst run is shown.
  • At a steady 50 searches a second, p95 stayed under 35 ms at every size we measured, except Typesense at 1M documents, where it was 148 to 156 ms.

Manage Engines

The instance API takes an org-scoped admin key. A key limited to chosen engines sees and manages only those engines, and cannot create one. The CLI has a command for each call except PATCH.

RequestWhat it does
GET /v1/instancesLists your engines.
GET /v1/instances/<id>Returns one engine, with its state and endpoint.
PATCH /v1/instances/<id>Changes name, lane, memoryMb or idleTimeoutS.
DELETE /v1/instances/<id>Deletes the engine and its data. It answers 409 instance_in_use while Headband indexes are still on the engine.
POST /v1/instances/<id>/wakeWakes a parked engine now.
POST /v1/instances/<id>/parkParks a serverless engine now. An always-on engine answers 409 always_on.
GET /v1/instances/<id>/wakesLists the engine's recent wakes, newest first. See Cold Starts below.

Indexes Through the REST API

To put an index on your engine, create it with instanceId. From then on, the REST API sends every request for that index to your engine: documents, settings and search all work as on the Indexes page.

This call takes a project-scoped admin key, like any index create. An org-wide key answers 403 project_scope_required, except the key the CLI gets from headband login.

terminal
curl -X POST https://headband.dev/v1/indexes \
  -H "Authorization: Bearer $HEADBAND_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uid": "products", "primaryKey": "id", "instanceId": "<id>"}'
202 response
{
  "taskUid": 1,
  "indexUid": "products",
  "engine": "meilisearch",
  "status": "succeeded",
  "instanceId": "<id>",
  "engineIndexUid": "hbp_<projectId>_products"
}

engineIndexUid is the index's name on your engine. It is the key's prefix plus the uid: hbp_<projectId>_ for a project key, hbo_<org>_ for the CLI's org key, and all lowercase on Elasticsearch. Use it, not the uid, when you call your engine's endpoint with the engine's own client. GET /v1/indexes and GET /v1/indexes/<uid> show it too. After a reindex it names the new index, for example hbp_<projectId>_products__v2. Read it from GET /v1/indexes/<uid> instead of hard-coding it.

With the CLI, the same create uses the key from headband login:

terminal
headband indexes create products --instance <id>
WhenAnswer
instanceId is not a string, or is empty400 invalid_instance_id
engine is left outThe index takes the engine's type.
engine differs from the engine's type400 instance_engine_mismatch
instanceId is shared. Shared engines are closed to new indexes.400 shared_engines_closed
instanceId is left out, the branch has no default engine of the index's type, and your plan's engines of that type are in use (Free: one of each on main, and one per branch for engines that park)402 plan_engine_limit
An Elasticsearch index on a branch other than main, on Free: Elasticsearch has no serverless lane, so a branch engine needs a paid plan402 plan_required
The index is in a project over your plan's project limit after a downgrade. Such a project is read-only: searches work, changes do not. Deleting an index still works402 plan_project_read_only
The engine was parked because your plan no longer includes it. Its data is kept. Wakes, resizes and requests answer this until an upgrade brings it back402 plan_downgrade_parked
instanceId is left out for a Visor index, your organization has no default Visor engine, and no engine host in the region runs Visor503 engine_not_available
The id is unknown, belongs to another organization, or your organization has no dedicated engines404 instance_not_found
The engine is being deleted409 instance_deleting
The engine has failed409 instance_failed
The engine is not ready yet503 instance_unavailable with Retry-After: 2

Indexes made on the endpoint stay there

An index you create directly on the endpoint, with the engine's own API, is not visible to the REST API. Create it with instanceId if you want to use it from both sides.

Connect to the Endpoint

Every engine has its own endpoint:

https://<id>.<region>.search.headband.dev

The engine's native API answers there, behind a Headband gateway that checks your key. Point the engine's official client at it, with a Headband API key where the client expects the engine's key.

Node
import { Meilisearch } from "meilisearch";

const client = new Meilisearch({
  host: "https://<id>.ewr1.search.headband.dev",
  apiKey: process.env.HEADBAND_API_KEY,
});

const results = await client.index("<index>").search("shoes");

The official clients send the key in these headers:

EngineHeader
MeilisearchAuthorization: Bearer <key>
TypesenseX-TYPESENSE-API-KEY: <key>
ElasticsearchAuthorization: ApiKey <key>
VisorAuthorization: Bearer <key>

The gateway reads any of these on any engine. It also takes Authorization: Basic with the key as the password. It never reads a key from the query string.

Typesense in the browser: send the key in a header

Typesense's browser client, SearchClient, sends the key as a query parameter by default, and the gateway answers that with 401 missing_credentials. Create it with sendApiKeyAsQueryParam: false. Typesense.Client already sends the header.

Only Headband API keys work. Engine-native credentials, such as Meilisearch tenant tokens or Typesense scoped search keys, answer 401 invalid_credentials. Health checks need a key too: GET /health on Meilisearch, Typesense and Visor, and GET / on Elasticsearch, take a search key.

Elasticsearch engines run version 8. Install @elastic/elasticsearch@8: client 9 sends a version 9 header that version 8 refuses. engineVersion on the engine shows the exact version.

Visor has no published client yet. Call its HTTP API directly. Index names are the engineIndexUid from the index, and are not lowercased. Pass mode as instant (the default) or retrieval for longer queries.

curl, Visor
curl -X POST "https://<id>.ewr1.search.headband.dev/v1/indexes/<engineIndexUid>/search" \
  -H "Authorization: Bearer $HEADBAND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "shoes", "mode": "instant", "limit": 20}'

Keys on the Endpoint

Engine endpoints take three kinds of key:

  • An org-scoped key works on every engine in your organization.
  • A project-scoped key works on the engines of its own branch. The project's default keys are main-branch keys, so they work on the engines behind the project's main indexes.
  • A key limited to chosen engines works on those engines only.

Any other engine, including one that belongs to another project, answers 401 invalid_credentials.

Org-scoped keys are always admin keys. There is no org-wide search key, because search keys ship in browsers. For a browser or native app, use a project search key: it searches the engines of its own branch and nothing else. The project's default search key is one, and the dashboard and CLI make more:

terminal
headband keys create storefront-search --type search --project <project id>

A key can also be limited to chosen engines with the Chosen dedicated engines scope in the dashboard, or with --instances <id> in the CLI (requires CLI 0.4.0). It works only on those engines' endpoints, and any other engine answers 401 invalid_credentials. See Keys for Chosen Engines.

A new key usually works on endpoints within a second. If that first push fails, the engine host picks the key up within a minute. A revoked key stops working on the same schedule.

The gateway sorts every request into a route class. An admin key reaches four of them. A search key reaches only search.

Route classFor exampleAdmin keySearch key
searchSearch, multi-search, healthYesYes
readList indexes, settings, stats, documents, tasksYesNo
writeAdd, update and delete documentsYesNo
manageCreate and delete indexes, change settingsYesNo
platformEngine keys, dumps, snapshots, metrics, cluster adminNoNo

So on an endpoint a search key cannot read settings, stats or tasks (as on the REST API), and also cannot list indexes. It gets 403 route_not_allowed. These are the search routes:

EngineSearch routes
Meilisearch/indexes/<uid>/search, /multi-search, /indexes/<uid>/facet-search, /indexes/<uid>/similar, /health
Typesense/collections/<name>/documents/search, /multi_search, /health
Elasticsearch/, _search, _msearch, _count, _field_caps, _validate/query, _explain, _rank_eval, scroll and point in time
Visor/v1/indexes/<uid>/search, /v1/search, /health

Platform routes are closed to every key, admin included. On Meilisearch these are keys, dumps, snapshots, metrics, logs, network, export and webhooks. On Typesense they are keys, operations, debug, config, metrics, conversations and analytics. On Elasticsearch, every route outside the other four classes is platform, such as _security, _nodes and _snapshot. On Visor they are /metrics and index maintenance (POST /v1/indexes/<uid>/maintain). A path the gateway does not know, a typo included, is platform too.

A key created with a narrower permission list reaches a class only when it holds every permission the class needs: search for search; documents.read for read; documents.add and documents.delete for write; and indexes.create, indexes.delete and settings.update for manage.

A search key reaches every index on its branch

On an engine's own endpoint, a search key can search every index on its branch's engines. A search key limited to chosen indexes, and a tenant token, work only through the REST API (/v1/search and /v1/multi-search): the engine endpoint refuses them with 401 invalid_credentials, because it cannot hold a key to one index or force a filter. See Tenant Tokens.

Cold Starts

A serverless engine parks after 300 idle seconds by default. The next request wakes it. The gateway holds that request until the engine is ready, then answers as usual. Requests through the REST API wake it the same way. Always-on engines never park.

Opening a connection to the endpoint can start a wake early, before any key is checked. An engine woken that way and then not used parks again after about a minute. A health check with a key is a request like any other, so a monitor that checks more often than the idle timeout keeps a serverless engine awake.

A response that waited for a wake carries a header:

header
x-headband-wake: warm;104

The first part says how the engine woke. The second is the wake time in milliseconds. A response that did not wait has no header. That includes a request whose own connection already woke the engine before the request arrived.

ValueMeaning
warmThe engine started without a restore first. This is the fast path.
restore_remoteThe data was restored from backup before the engine started. This wake is slower.
bucket_coldThe engine's disk was loaded from storage, because its host held no copy of it. This wake is slower.
vm_restoreA serverless Typesense engine resumed from its memory snapshot.
cold_bootA serverless Typesense engine booted without a snapshot to resume from.

In our tests a warm wake took about 100 ms. The gateway holds a request while the engine wakes: 10 seconds on Free, 25 seconds on paid plans. If the wake takes longer, it answers 503 instance_waking with Retry-After: 2. Retry after that pause. An engine still loading a large index answers at once, with a longer Retry-After: see Start Times.

Those holds apply on the engine's own endpoint. A request through the Headband REST API waits for your plan's full hold: 10 seconds on Free, 30 seconds on paid plans.

To see how past wakes went, call GET /v1/instances/<id>/wakes. It returns the 50 most recent wakes, newest first. Pass ?limit= for up to 200.

response
{
  "data": [
    {
      "requestedAt": "2026-10-12T14:03:11.204Z",
      "readyAt": "2026-10-12T14:03:11.308Z",
      "firstByteAt": "2026-10-12T14:03:11.315Z",
      "via": "warm",
      "outcome": "served",
      "readyMs": 104,
      "phases": {}
    }
  ]
}

via says how the engine woke, as in the table above. outcome is ready, served, timeout or failed. readyMs is null until the engine is ready, and phases holds the box's per-step measurements, such as spawnMs, when it reports them.

To wake an engine before traffic arrives, run headband instances wake <id> or call POST /v1/instances/<id>/wake.

Start Times

An engine starts after a restart, a resize, a wake, a host reboot or a move to another host. Most engines start in seconds. Typesense keeps its vector indexes in memory, and saves them to disk when it stops, so its next start reads them back instead of rebuilding them. A start with no saved copy, such as an engine's first start after we upgrade Typesense, rebuilds every index, about 6 minutes per GB of data on disk in our tests, and saves them at its end. So does a start after more than about a tenth of the vectors changed since the copy was saved.

In our tests each engine had 6 CPUs, as an engine of 4 GB or more has, and distinct random 512-dimension vectors, the hardest case for Typesense's vector index. Real embeddings usually load faster. Typesense's data on disk includes its saved vector index, about 2.2 KB per vector:

EngineVectorsData on diskStart
Typesense100,0001.2 GB5 seconds
Typesense300,0001.8 GB11 seconds
Typesense700,0004.1 GB25 to 27 seconds
Typesense, rebuilding700,0002.1 GB16 minutes
Elasticsearch300,0001.5 GB9 seconds
Meilisearch300,0001.5 GBunder 1 second

An always-on Typesense engine of 512 MB or 1 GB starts on 4 CPUs instead of its usual 2 when the host has CPU to spare. With 50,000 vectors, a 1 GB engine started in 49 seconds instead of 95.

A Typesense engine restarted right after a large import also replays the writes it had not saved yet. In our 700,000-vector test that added 40 seconds.

A Visor engine prepares each index for its first search at every start, a wake included. With 130,000 and 725,000 short documents (1.3 GB) on an 8 GB engine that took about 2 seconds, the larger index setting the time. Visor 0.1.2 does it about ten times faster. Until it is done the engine shows starting, so no search pays for it under its own deadline.

While an engine loads, the dashboard and GET /v1/instances/<id> show state: "starting", with starting.usualS, how long a start at its size usually takes. Once a start has run longer than your plan's hold, requests answer 503 instance_waking at once, with a Retry-After of the usual time left, from 5 to 60 seconds.

Don't restart an engine that is starting: a restart starts the load over. Headband restarts an engine itself only when its process exits, or stops answering.

Warm Before Traffic

Warming gets sleeping engines ready before your users arrive, by index name or for a whole project. It answers at once and never waits for an engine. Warming is free: it counts no searches. Each key can warm up to 60 times a minute.

POST/v1/warm

Warm up to 100 indexes by name, or every index of a project. level is optional.

The key needs the search permission. A project-scoped key reaches its own project only.

Auth admin or search keyReturns 202
Request body
{ "indexes": ["products", "articles"] }
{ "project": "prj_123", "branch": "main", "level": "stage" }
Response
{
  "level": "auto",
  "indexes": [
    { "index": "products", "engine": "meilisearch", "state": "starting" },
    { "index": "articles", "engine": "typesense", "state": "staged", "reason": "start_unclaimed" }
  ]
}
LevelWhat it does
stageLoads the engine's data onto its server, so its next wake reads nothing from storage.
startStages, then starts the engine. A start that no request uses within two minutes goes back to sleep and is not billed.
autoThe default. Starts an engine woken in the last 24 hours and stages an older one. An always-on engine needs nothing.

The answer has one row per index, and indexes on one engine share one warm. Its state is awake, staging, staged, starting or refused, and reason says why when there is one. An index on Headband's shared engines is always awake. A refused row did nothing, and its reason is one of these:

ReasonWhy nothing was done
not_foundThere is no such index, or the key cannot reach it.
instance_deletingThe index's engine is being deleted.
instance_failedThe engine has failed.
plan_downgrade_parkedThe engine was parked because your plan no longer includes it. Upgrade to wake it.
instance_busyThe engine is being set up or is going to sleep. Try again in a moment.
box_unavailableThe engine's server cannot be reached now.
stage_unsupportedThe engine's data is not on Headband's storage yet, and its disk is not on its server, so there is nothing to load ahead.
stage_failedThe data could not be read from storage. The next wake fetches what it needs.
no_cache_roomThe server has no room for the data now.

Three starts in a row that no request uses make the next hour's starts stage only, with start_unclaimed. Naming a branch other than main is 403 branch_not_in_key.

Package versions

headband warm requires CLI 0.4.0. client.warm() needs @headband/react 0.2.0. On 0.1.1, call POST /v1/warm from the browser directly.
React SDK
import { headband, HeadbandError } from "@headband/react";

const client = headband("https://headband.dev", "hb_src_YOUR_SEARCH_KEY");
try {
  await client.warm({ indexes: ["products"] });
  await client.warm({ project: "prj_123" }, { level: "start" });
} catch (err) {
  // Over 60 warms a minute for this key: a warm is only a hint, so skip it.
  if (!(err instanceof HeadbandError && err.status === 429)) throw err;
}
terminal
headband warm products articles
headband warm --project prj_123 --level start

Calls From the Browser

Browser code can call an endpoint directly with a project search key. The gateway answers CORS preflights itself, before it checks any key, and every response lets any origin read it.

Browser, Meilisearch
const endpoint = "https://<id>.ewr1.search.headband.dev";
const searchKey = "<project search key>";

const res = await fetch(`${endpoint}/indexes/<index>/search`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${searchKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ q: "shoes" }),
});
const { hits } = await res.json();

For Typesense, create the SearchClient with sendApiKeyAsQueryParam: false, or every call answers 401 missing_credentials:

Browser, Typesense
import Typesense from "typesense";

const client = new Typesense.SearchClient({
  nodes: [{ host: "<id>.ewr1.search.headband.dev", port: 443, protocol: "https" }],
  apiKey: "<project search key>",
  // The gateway reads the key only from a header.
  sendApiKeyAsQueryParam: false,
});

const results = await client
  .collections("<collection>")
  .documents()
  .search({ q: "shoes", query_by: "<field>" });

The preflight answer looks like this:

preflight response
HTTP/1.1 204 No Content
access-control-allow-origin: *
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
access-control-allow-headers: authorization, content-type
access-control-max-age: 86400

access-control-allow-headers repeats the headers the browser asked for. Browser code can read x-headband-wake. It can also read Retry-After whenever a gateway 429 or 503 carries one.

The gateway never allows credentials, so do not send cookies with credentials: "include". Never put an admin key in browser code.

Gateway Errors

When the gateway itself refuses a request, it answers with JSON. message repeats error, so the official clients show it. Errors from the engine pass through untouched, in the engine's own shape.

403 response
{
  "error": "route not allowed for this key",
  "code": "route_not_allowed",
  "class": "read",
  "message": "route not allowed for this key"
}
StatusCodeWhat it means
400api_identity_requiredNever for a request through your engine endpoint. The gateway sends it only on the box's API router socket, when the router forwards a request without naming the organization, key and plan it checked.
401missing_credentialsNo key in a header the gateway reads. A key in the query string is ignored, so ?x-typesense-api-key= on its own lands here too. Typesense's SearchClient sends that by default: create it with sendApiKeyAsQueryParam: false.
401invalid_credentialsThe key is unknown, expired or revoked, belongs to another organization, or cannot reach this engine: a project key belongs to another project or branch, or the key is limited to other engines.
403route_not_allowedThis key cannot call this route. The body's class names the route class: search, read, write, manage or platform. Platform routes, and paths the gateway does not know, are closed to every key.
403internal_key_requiredNever for a request through your engine endpoint. The gateway sends it only on the box's internal socket, to the box's own Sync service when it calls without an internal key.
404instance_not_foundNo engine answers at this host, or it is being deleted.
405method_not_allowedCONNECT, or a protocol upgrade such as a WebSocket. No engine API needs either.
408request_timeoutThe body of a search or read stopped arriving for 30 seconds before it was complete. Send the request again.
413payload_too_largeThe request body is over 256 MiB. Send it in smaller batches.
429too_many_requestsThe engine already has your plan's limit of requests in flight (4 on Free, 8 on Starter, 32 on Pro, 128 on Scale), counting requests that wait for a wake. Retry after Retry-After, which is 2 seconds.
429too_many_auth_failuresToo many failed authentications from your network address. Wait the seconds in Retry-After.
431request_header_fields_too_largeThe request has 197 or more header fields. Send it with fewer headers.
500internalThe gateway failed while handling the request. Retry it.
502engine_unreachableThe gateway could not reach the engine, or lost it partway through the request. For a write, the outcome is unknown: the engine may have applied it. Check before you send it again, for example by reading the document back. An Elasticsearch write without an explicit id, such as POST /{index}/_doc, stores a second copy if it is sent again, and the official Elasticsearch and Typesense clients retry a 502 by default. Give writes explicit ids, so a repeat overwrites the document instead of adding another.
503instance_busyThe engine is busy, for example finishing a backup, and could not take the request in time. Retry after the seconds in Retry-After: 2, unless the engine host sends its own.
503lease_expiringThe engine host is renewing its hold on the engine's storage, which takes a few seconds. The body's error reads instance storage lease is being renewed, retry shortly. Retry after Retry-After, which is 2 seconds.
503instance_movingA write waited while the engine moved to a new size, and the move took longer than the wait. Nothing was sent to the engine, so sending it again is safe. Retry after Retry-After, which is 2 seconds.
503instance_fencingA write (anything but a search or a read) was refused before it was sent to the engine, while its host was handing off or renewing the engine's storage. Nothing was applied, so sending it again is safe. The body's error reads instance storage is being fenced; the write was not acknowledged, retry shortly. Retry after Retry-After, which is 2 seconds. A write cut after it reached the engine gets a 502 engine_unreachable instead, and one cut after its answer had started arrives as a dropped connection. In both cases the outcome is unknown.
503instance_read_onlyThe engine's host cannot reach Headband's control plane, so it takes no writes. Searches and reads keep working. The write was refused before it reached the engine, so send it again. Writes resume by themselves once the control plane is back. Task status reads, Meilisearch's /tasks and /batches and Elasticsearch's _tasks, get this answer too. The body's error reads the control plane cannot be reached; searches work and writes resume by themselves. Retry after Retry-After, which is 10 seconds.
503instance_wakingThe engine took longer to wake than your plan's hold: 10 seconds on Free, 25 seconds on paid plans. A request through the Headband REST API waits up to 30 seconds on a paid plan. Retry after Retry-After, which is 2 seconds. An engine loading a large index after a restart, resize, wake or move answers this code at once, without the hold, once its start has outlasted the hold. Its body says instance is starting and carries starting: when the start began, and usualS, how long a start at this size usually takes. Its Retry-After is the usual time left, from 5 to 60 seconds. A Typesense engine with 700,000 vectors takes about 25 seconds, or about 16 minutes when it has to rebuild its vector index. See start times.
503instance_unavailableThe engine could not be started or reached right now, or its host is restarting. Retry after Retry-After when it is sent.
504engine_timeoutThe engine took too long: 30 seconds without data for a search or read, or 10 minutes in total for anything else.

The same list is on the Errors page.

Limits

LimitValueOver it
Request body256 MiB413 payload_too_large
Requests in flight per engineFree 4, Starter 8, Pro 32, Scale 128, counting those waiting for a wake429 too_many_requests
Wait for a wake10 seconds on Free, 25 seconds on paid plans. Through the REST API, 30 seconds on paid plans503 instance_waking
Search and read requests30 seconds without data from the engine504 engine_timeout
Other requests10 minutes in total504 engine_timeout
Failed authenticationsMore than 30 in a 60-second window, per IPv4 address or IPv6 /64429 too_many_auth_failures

A client over the failed-authentication limit can still use a key the gateway verified in the last minute. Platform routes are closed to every key, as Keys on the Endpoint describes.