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
- Open Engines in the sidebar.
- Click Create engine.
- 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.
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
/v1/instancesCreate a dedicated engine. Only engine is required.
Needs an org-wide admin key. A project-scoped key answers 403 org_key_required.
{
"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
}{
"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
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.
| Engine | Lanes | Sizes (memory) | Default |
|---|---|---|---|
| Meilisearch | Serverless, always on | 512 MB, 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GB | Serverless, 512 MB |
| Elasticsearch | Always on | 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GB | Always on, 1 GB |
| Typesense | Always on; serverless in early access | 512 MB, 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GB | Always on, 512 MB |
| Visor | Serverless, always on | 512 MB, 1 GB, 2 GB, 4 GB, 8 GB, 16 GB, 32 GB | Serverless, 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.
| Engine | Size | 1M docs: searches a second | p95 | 100K docs: searches a second | p95 |
|---|---|---|---|---|---|
| Meilisearch | 512 MB | 320 | 82 ms | ||
| Meilisearch | 1 GB | Does not fit | 2,562 | 1.9 ms | |
| Meilisearch | 2 GB | 143 | 72 ms | 4,302 | 1.4 ms |
| Meilisearch | 4 GB | 308 | 28 ms | ||
| Meilisearch | 8 GB | 1,799 | 6.2 ms | ||
| Typesense | 512 MB | 400 | 59 ms | ||
| Typesense | 1 GB | Not measured | 406 | 59 ms | |
| Typesense | 2 GB | 106 | 151 ms | 798 | 16 ms |
| Typesense | 4 GB | 110 | 146 ms | ||
| Typesense | 8 GB | Not measured | |||
| Elasticsearch | 1 GB | Not measured | 283 | 67 ms | |
| Elasticsearch | 2 GB | 122 | 68 ms | 782 | 9.3 ms |
| Elasticsearch | 4 GB | 273 | 27 ms | ||
| Elasticsearch | 8 GB | 268 | 27 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.
| Request | What it does |
|---|---|
GET /v1/instances | Lists 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>/wake | Wakes a parked engine now. |
POST /v1/instances/<id>/park | Parks a serverless engine now. An always-on engine answers 409 always_on. |
GET /v1/instances/<id>/wakes | Lists 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.
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>"}'{
"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:
headband indexes create products --instance <id>| When | Answer |
|---|---|
instanceId is not a string, or is empty | 400 invalid_instance_id |
engine is left out | The index takes the engine's type. |
engine differs from the engine's type | 400 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 plan | 402 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 works | 402 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 back | 402 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 Visor | 503 engine_not_available |
| The id is unknown, belongs to another organization, or your organization has no dedicated engines | 404 instance_not_found |
| The engine is being deleted | 409 instance_deleting |
| The engine has failed | 409 instance_failed |
| The engine is not ready yet | 503 instance_unavailable with Retry-After: 2 |
Indexes made on the endpoint stay there
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.devThe 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.
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:
| Engine | Header |
|---|---|
| Meilisearch | Authorization: Bearer <key> |
| Typesense | X-TYPESENSE-API-KEY: <key> |
| Elasticsearch | Authorization: ApiKey <key> |
| Visor | Authorization: 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
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 -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:
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 class | For example | Admin key | Search key |
|---|---|---|---|
| search | Search, multi-search, health | Yes | Yes |
| read | List indexes, settings, stats, documents, tasks | Yes | No |
| write | Add, update and delete documents | Yes | No |
| manage | Create and delete indexes, change settings | Yes | No |
| platform | Engine keys, dumps, snapshots, metrics, cluster admin | No | No |
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:
| Engine | Search 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
/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:
x-headband-wake: warm;104The 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.
| Value | Meaning |
|---|---|
warm | The engine started without a restore first. This is the fast path. |
restore_remote | The data was restored from backup before the engine started. This wake is slower. |
bucket_cold | The engine's disk was loaded from storage, because its host held no copy of it. This wake is slower. |
vm_restore | A serverless Typesense engine resumed from its memory snapshot. |
cold_boot | A 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.
{
"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:
| Engine | Vectors | Data on disk | Start |
|---|---|---|---|
| Typesense | 100,000 | 1.2 GB | 5 seconds |
| Typesense | 300,000 | 1.8 GB | 11 seconds |
| Typesense | 700,000 | 4.1 GB | 25 to 27 seconds |
| Typesense, rebuilding | 700,000 | 2.1 GB | 16 minutes |
| Elasticsearch | 300,000 | 1.5 GB | 9 seconds |
| Meilisearch | 300,000 | 1.5 GB | under 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.
/v1/warmWarm 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.
{ "indexes": ["products", "articles"] }
{ "project": "prj_123", "branch": "main", "level": "stage" }{
"level": "auto",
"indexes": [
{ "index": "products", "engine": "meilisearch", "state": "starting" },
{ "index": "articles", "engine": "typesense", "state": "staged", "reason": "start_unclaimed" }
]
}| Level | What it does |
|---|---|
stage | Loads the engine's data onto its server, so its next wake reads nothing from storage. |
start | Stages, then starts the engine. A start that no request uses within two minutes goes back to sleep and is not billed. |
auto | The 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:
| Reason | Why nothing was done |
|---|---|
not_found | There is no such index, or the key cannot reach it. |
instance_deleting | The index's engine is being deleted. |
instance_failed | The engine has failed. |
plan_downgrade_parked | The engine was parked because your plan no longer includes it. Upgrade to wake it. |
instance_busy | The engine is being set up or is going to sleep. Try again in a moment. |
box_unavailable | The engine's server cannot be reached now. |
stage_unsupported | The 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_failed | The data could not be read from storage. The next wake fetches what it needs. |
no_cache_room | The 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.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;
}headband warm products articles
headband warm --project prj_123 --level startCalls 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.
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:
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:
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: 86400access-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.
{
"error": "route not allowed for this key",
"code": "route_not_allowed",
"class": "read",
"message": "route not allowed for this key"
}| Status | Code | What it means |
|---|---|---|
| 400 | api_identity_required | Never 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. |
| 401 | missing_credentials | No 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. |
| 401 | invalid_credentials | The 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. |
| 403 | route_not_allowed | This 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. |
| 403 | internal_key_required | Never 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. |
| 404 | instance_not_found | No engine answers at this host, or it is being deleted. |
| 405 | method_not_allowed | CONNECT, or a protocol upgrade such as a WebSocket. No engine API needs either. |
| 408 | request_timeout | The body of a search or read stopped arriving for 30 seconds before it was complete. Send the request again. |
| 413 | payload_too_large | The request body is over 256 MiB. Send it in smaller batches. |
| 429 | too_many_requests | The 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. |
| 429 | too_many_auth_failures | Too many failed authentications from your network address. Wait the seconds in Retry-After. |
| 431 | request_header_fields_too_large | The request has 197 or more header fields. Send it with fewer headers. |
| 500 | internal | The gateway failed while handling the request. Retry it. |
| 502 | engine_unreachable | The 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. |
| 503 | instance_busy | The 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. |
| 503 | lease_expiring | The 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. |
| 503 | instance_moving | A 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. |
| 503 | instance_fencing | A 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. |
| 503 | instance_read_only | The 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. |
| 503 | instance_waking | The 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. |
| 503 | instance_unavailable | The engine could not be started or reached right now, or its host is restarting. Retry after Retry-After when it is sent. |
| 504 | engine_timeout | The 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
| Limit | Value | Over it |
|---|---|---|
| Request body | 256 MiB | 413 payload_too_large |
| Requests in flight per engine | Free 4, Starter 8, Pro 32, Scale 128, counting those waiting for a wake | 429 too_many_requests |
| Wait for a wake | 10 seconds on Free, 25 seconds on paid plans. Through the REST API, 30 seconds on paid plans | 503 instance_waking |
| Search and read requests | 30 seconds without data from the engine | 504 engine_timeout |
| Other requests | 10 minutes in total | 504 engine_timeout |
| Failed authentications | More than 30 in a 60-second window, per IPv4 address or IPv6 /64 | 429 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.