Errors in the Aetherfy vector database
The Aetherfy error envelope
Every error the Aetherfy vector API authors is returned in one shape:
{
"error": {
"code": "STABLE_CODE",
"message": "human readable description"
}
}code is the stable, machine-readable identifier — branch on it, never on the
message text, which is written for humans and may be reworded. Alongside the two
required fields, an Aetherfy error may carry extras that depend on the site that
raised it:
| Extra field | Appears on |
|---|---|
details | Errors with structured context |
field, max | Validation failures against a numeric cap |
request_id | Correlation identifier for support |
documentation_url | Errors with a canonical explanation |
collection_name | Collection-scoped failures |
existing_regions | A collection that lives in another region |
agents | A collection still referenced by agents |
size_mb, max_size_mb | Response-size failures |
current_etag | Schema compare-and-set conflicts |
retriable | Errors Aetherfy marks as safe to retry |
The Aetherfy error envelope is universal
Every path authors the envelope, including the storage-engine pass-through
routes. Those routes proxy Qdrant, whose native failure body is shaped
{"status": {"error": "..."}, "time": 0.001} — Aetherfy normalises it on the way
out rather than relaying it, so error.code is always present and a generic
handler can rely on it.
Earlier versions of these docs described POST /api/v1/collections/{name}/points/retrieve
as an exception that relayed the engine body verbatim. That is no longer true.
If you wrote a handler tolerating a missing error.code on that route, the
tolerance is now dead code — harmless, but you can drop it.
Error codes in the Aetherfy vector API
Every code the Aetherfy vector API returns, with its HTTP status and whether
retrying the identical request can succeed. Aetherfy’s agent and platform
surfaces have codes of their own — PLAN_LIMIT_EXCEEDED, RUNTIME_IMMUTABLE,
the AGENT_* and WORKSPACE_* families, SOFT_CAP_EXCEEDED, DUNNING_FROZEN
— which are documented on the pages that raise them:
/platform/limits, /platform/billing,
/platform/workspaces and
/agents/managing. Every Aetherfy code from every surface is
also published as machine-readable data at errors.json, the
catalogue described on Using AI with Aetherfy.
| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
MISSING_API_KEY | 401 | No Authorization header, or not a Bearer header | no |
INVALID_API_KEY | 401 | The key does not resolve | no |
ACCOUNT_SUSPENDED | 403 | The account is suspended for billing | no |
COLLECTION_OWNERSHIP_MISMATCH | 403 | The collection belongs to another account | no |
NOT_FOUND | 404 | Unsupported endpoint, or missing collection | no |
COLLECTION_NOT_FOUND | 404 | No such collection | no |
SCHEMA_NOT_DEFINED | 404 | The collection has no stored schema. Both SDKs absorb this one: get_schema returns None / getSchema resolves null rather than raising | no |
METHOD_NOT_ALLOWED | 405 | The method is not supported on that path | no |
VALIDATION_ERROR | 400 | A request field failed validation | no |
INVALID_POINT_ID | 400 | A point id is not an unsigned integer or UUID | no |
TOO_MANY_POINTS | 400 | More than 10 000 points in one upsert | no |
RESERVED_FIELD | 400 | A payload key begins __aetherfy_ | no |
MALFORMED_JSON | 400 | The body is not valid JSON | no |
SCHEMA_VALIDATION_FAILED | 400 | The payload violates a strictly-enforced schema | no |
COLLECTION_LIMIT_EXCEEDED | 400 | The plan’s collection cap is reached | no |
INVALID_ENFORCEMENT_MODE | 400 | Not off, warn, or strict | no |
INVALID_SAMPLE_SIZE | 400 | Not an integer between 100 and 10 000. Shared by analyze and conformance | no |
INVALID_SCAN_MODE | 400 | A conformance scan mode other than sample or full | no |
INVALID_MAX_EXAMPLES | 400 | A conformance max_examples outside 0 to 100 | no |
REQUEST_IDLE_TIMEOUT | 408 | 30 s elapsed with no bytes on an upsert | no |
COLLECTION_EXISTS_IN_OTHER_REGION | 409 | The collection already lives elsewhere | no |
COLLECTION_NAME_TAKEN | 409 | The name is in use by a collection with a different configuration — a re-create with the same configuration is idempotent and returns 200 | no |
COLLECTION_IN_USE | 409 | An agent read or wrote the collection within the last 35 days. Nothing is declared — see the rule | no |
CLEANUP_IN_PROGRESS | 409 | A prior deletion is still finishing — carries retriable: true | yes |
SCHEMA_VERSION_MISMATCH | 412 | If-Match ETag conflict; body carries current_etag | no |
PAYLOAD_TOO_LARGE | 400 / 413 | Two causes, two statuses: 400 when a single point’s payload exceeds 64 KB, 413 when the upsert wire body exceeds the 500 MB ceiling | no |
RESPONSE_TOO_LARGE | 413 | The response would exceed 10 MB | no |
COLLECTION_REGIONS_EMPTY | 422 | No regions resolved for the request | no |
COLLECTION_REGIONS_NOT_IN_SCOPE | 422 | Requested regions are outside the plan’s scope | no |
STARTER_REGION_CONSISTENCY | 422 | Single-region plan; the resource asked for a different region | no |
RATE_LIMIT_EXCEEDED | 429 | The plan’s per-minute request cap is exceeded | yes |
STORAGE_LIMIT_EXCEEDED | 429 | The plan’s storage cap is exceeded — free space first | no |
AUTHENTICATION_ERROR | 500 | Authentication itself failed, rather than the key being rejected | yes |
INTERNAL_ERROR | 500 | Unhandled server-side failure | no |
PROXY_ERROR | 502 | The proxy could not complete the upstream call | yes |
SERVICE_UNAVAILABLE | 503 | The service is temporarily unable to serve | yes |
GATEWAY_TIMEOUT | 504 | The upstream call timed out | yes |
How recently counts as “in use”
COLLECTION_IN_USE is the one code above whose meaning depends on a clock, so
the rule is worth stating exactly: an agent blocks a delete for 35 days after
the last time it read or wrote the collection. Older than that and it no
longer blocks anything.
There is nothing to configure and nothing to declare. Aetherfy records which
collections each agent actually reads and writes, so a collection your agent
code created at runtime is protected exactly like one it has used since its
first deploy. The 409 body’s agents array names the agents currently blocking
the delete.
Two consequences worth knowing before you script against it:
- An agent that runs monthly still blocks its collection. That is what the 35 days is for — the longest month, plus a few days of slack for a paused agent or a run that slipped. A scheduled task that fires on the 1st is inside the window on the 31st.
- A brand-new agent blocks nothing yet. Until it has actually opened the collection, there is nothing to observe, and the delete succeeds. “No agent has touched this collection” and “no agent uses this collection” are different statements, and only the first one is ever checked.
Aetherfy’s fallback error codes
The codes above are authored deliberately: something in Aetherfy decided that exact failure and named it. The codes below are different in kind. Aetherfy proxies a storage engine and authors the error envelope on the way out — when the upstream body carries a usable code, that code is preserved; when it does not, Aetherfy synthesises one from the HTTP status.
Seeing one of these means the upstream failed without naming its own reason,
so the message carries more signal than the code. Treat them as “something
upstream went wrong, here is roughly what kind” rather than as specific
diagnoses.
| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
BAD_REQUEST | 400 | The engine rejected the request and supplied no code. Read the message | no |
UNAUTHORIZED | 401 | An upstream component refused credentials without naming a code | no |
FORBIDDEN | 403 | An upstream component denied the request without naming a code | no |
CONFLICT | 409 | A conflicting state upstream, unnamed. The specific 409s above are more precise | no |
PRECONDITION_FAILED | 412 | A precondition failed upstream, unnamed — typically an ETag mismatch | no |
RATE_LIMITED | 429 | A 429 from upstream carrying no code of its own | yes |
UPSTREAM_ERROR | 500 / 502 / 503 / 504 | The catch-all: an unmapped upstream status with no code | yes |
RATE_LIMITED is not RATE_LIMIT_EXCEEDED
Both exist, both are 429, and they mean different things. Branch on the code, not the status:
| Code | Who produced it | What it tells you |
|---|---|---|
RATE_LIMIT_EXCEEDED | Aetherfy’s own per-minute rate limiter | You exceeded your plan’s request allowance. Back off; the allowance resets on the next minute bucket |
RATE_LIMITED | The envelope normaliser, from an upstream 429 | Something upstream of Aetherfy throttled the request without naming a code. Not necessarily your plan allowance |
A retry loop keyed on status === 429 will treat these identically, which is
usually fine — both want backoff. A dashboard or alert that reports “you hit your
rate limit” should key on RATE_LIMIT_EXCEEDED specifically, or it will
misattribute an upstream throttle to the customer’s plan.
Which Aetherfy errors are worth retrying
Seven Aetherfy codes can succeed on an identical retry — five that Aetherfy authors deliberately, and two that the envelope normaliser synthesises from an upstream failure. Every other code on this page is a statement about the request or the account, and repeating it verbatim produces the same answer.
| Retryable code | HTTP | How to retry against Aetherfy |
|---|---|---|
CLEANUP_IN_PROGRESS | 409 | Wait for the prior deletion to finish; the body carries retriable: true |
RATE_LIMIT_EXCEEDED | 429 | Back off on your own schedule — Aetherfy sends no Retry-After header on the vector API |
PROXY_ERROR | 502 | Retry with backoff |
SERVICE_UNAVAILABLE | 503 | Retry with backoff |
GATEWAY_TIMEOUT | 504 | Retry with backoff |
RATE_LIMITED | 429 | A throttle upstream of Aetherfy, not your plan allowance. Back off with jitter |
UPSTREAM_ERROR | 500 / 502 / 503 / 504 | An unmapped upstream failure. Retry with backoff, and read the message |
The two normaliser codes are worth handling separately from their authored
twins: they carry no diagnosis of their own, so a retry that keeps failing on
UPSTREAM_ERROR or RATE_LIMITED needs its message read rather than its
backoff lengthened.
STORAGE_LIMIT_EXCEEDED is a 429 but is not retryable: nothing frees itself
while you wait. Delete data or raise the cap. See limits.
Exception classes in the Aetherfy Python SDK
The Aetherfy Python SDK maps API errors onto typed exceptions. The base class is
AetherfyVectorsException; catch it to catch everything the SDK authors.
| Attribute on the base class | Meaning |
|---|---|
.message | Human-readable description |
.request_id | Correlation identifier |
.status_code | HTTP status |
.details | Structured context, when present |
.error_code | The stable code from the envelope |
| Exception | Extra attributes |
|---|---|
AetherfyVectorsException | base class |
AuthenticationError | — |
RateLimitExceededError | .retry_after |
ServiceUnavailableError | — |
ValidationError | — |
CollectionNotFoundError | — |
PointNotFoundError | — |
RequestTimeoutError | — |
NetworkError | — |
ConflictError | — (any 409 without a more specific class; the backend’s code is in .error_code) |
SchemaValidationError | .errors |
SchemaNotFoundError | — |
CollectionInUseError | — |
CollectionInOtherRegionError | .existing_regions, .requesting_region |
QuotaExceededError | .quota_type, .current, .limit |
PartialUpsertError | .saved, .total, .failed |
.retry_after on RateLimitExceededError is an attribute of the class, not a
value Aetherfy supplies: Aetherfy sends no Retry-After header on a vector-API
429, and this SDK only ever calls the vector API, so there is nothing to populate
it from. Do not branch on it — back off on your own schedule.
The helper is_retryable_error(error) lives in aetherfy_vectors.exceptions and
answers the retry question for you.
import os
import time
from aetherfy_vectors import (
AetherfyVectorsClient,
AetherfyVectorsException,
AuthenticationError,
QuotaExceededError,
RateLimitExceededError,
)
from aetherfy_vectors.exceptions import is_retryable_error
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
for attempt in range(3):
try:
results = client.search("articles", query_vector=[0.1, 0.2, 0.3, 0.4], limit=5)
print([hit.id for hit in results])
break
except AuthenticationError as err:
print("bad key, not retrying:", err.message)
break
except QuotaExceededError as err:
print("quota:", err.quota_type, err.current, err.limit)
break
except RateLimitExceededError as err:
print("rate limited, request_id:", err.request_id)
time.sleep(2 ** attempt)
except AetherfyVectorsException as err:
if not is_retryable_error(err):
print("permanent failure:", err.error_code, err.status_code)
break
time.sleep(2 ** attempt)
client.close()Exception classes in the Aetherfy JavaScript SDK
The Aetherfy JavaScript base class is AetherfyVectorsError — note the different
name from Python’s AetherfyVectorsException.
| Member on the base class | Meaning |
|---|---|
.code | The stable code from the envelope |
.requestId | Correlation identifier |
.statusCode | HTTP status |
.details | Structured context, when present |
toJSON() | Serialisable form of the error |
| Exception | Extra members |
|---|---|
AetherfyVectorsError | base class |
AuthenticationError | — |
RateLimitExceededError | .retryAfter |
ServiceUnavailableError | — |
ValidationError | — |
CollectionNotFoundError | — |
PointNotFoundError | — |
RequestTimeoutError | — |
NetworkError | — |
ConflictError | — |
CollectionInOtherRegionError | — |
QuotaExceededError | — |
CollectionInUseError | — |
SchemaNotFoundError | — |
SchemaValidationError | — |
PartialUpsertError | — |
.retryAfter on the Aetherfy JavaScript RateLimitExceededError is the same
story as Python’s .retry_after: the member exists on the class, but because
Aetherfy sends no Retry-After header on a vector-API 429 there is nothing to
populate it from. Do not branch on it — back off on your own schedule.
Three utilities ship alongside them from the Aetherfy package root:
createErrorFromResponse, isAetherfyVectorsError, and isRetryableError.
import {
AetherfyVectorsClient,
AetherfyVectorsError,
AuthenticationError,
QuotaExceededError,
RateLimitExceededError,
isAetherfyVectorsError,
isRetryableError,
} from 'aetherfy-vectors';
const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], { limit: 5 });
console.log(results.map((hit) => hit.id));
break;
} catch (err) {
if (err instanceof AuthenticationError) {
console.log('bad key, not retrying');
break;
}
if (err instanceof QuotaExceededError) {
console.log('quota exceeded');
break;
}
if (err instanceof RateLimitExceededError) {
console.log('rate limited, requestId:', err.requestId);
await sleep(2 ** attempt * 1000);
continue;
}
if (isAetherfyVectorsError(err)) {
if (!isRetryableError(err)) {
const e = err as AetherfyVectorsError;
console.log('permanent failure:', e.code, e.statusCode);
break;
}
await sleep(2 ** attempt * 1000);
continue;
}
throw err;
}
}
client.dispose();The duplicate AuthenticationError in the Aetherfy JavaScript SDK
The Aetherfy JavaScript package defines two classes named
AuthenticationError, in two modules. To keep the package root unambiguous, the
one declared in src/auth.ts is re-exported under a different name:
| Declared in | Root export name |
|---|---|
src/exceptions.ts | AuthenticationError |
src/auth.ts | AuthError |
So AuthenticationError imported from the Aetherfy package root is the
transport-level error raised when the API rejects a key, and AuthError is the
key-management one. An instanceof check against the wrong class silently never
matches, so import the name that corresponds to the layer you are catching.
Mapping Aetherfy error names across the two SDKs
| Concept | Python | JavaScript |
|---|---|---|
| Base class | AetherfyVectorsException | AetherfyVectorsError |
| Stable code | .error_code | .code |
| Correlation id | .request_id | .requestId |
| HTTP status | .status_code | .statusCode |
Rate-limit attribute (never populated — no Retry-After header on the Aetherfy vector API) | .retry_after | .retryAfter |
| Retryability helper | is_retryable_error (in aetherfy_vectors.exceptions) | isRetryableError (package root) |
| Type guard | — | isAetherfyVectorsError |
| Conflict class | ConflictError | ConflictError |
The caps and plan limits behind most 4xx codes are on limits; the routes that raise them are on the REST API reference.