SurrealDB Agent Memory uses standard HTTP status codes, and every REST error response carries the same small JSON body.
Error response format
All errors return a JSON body with a single field:
{
"message": "context acme-staging does not exist"
}| Field | Description |
|---|---|
message | A human-readable explanation of this error. |
The status is sent only as the HTTP status code of the response. The body has no status, type, title or instance field.
HTTP status codes
400 Bad Request
The request body or query parameters are invalid. Common causes:
Malformed JSON body
Missing required fields
Invalid field values, such as a malformed scope path or pattern, label, entity reference, pagination cursor or model configuration
A write to a scope that is not registered
An empty file upload
{
"message": "the uploaded file is empty"
}401 Unauthorized
No Authorization: Bearer token was provided, or the key is malformed, unknown or expired.
{
"message": "missing or invalid Authorization bearer token"
}403 Forbidden
The API key is valid but does not have permission to perform this operation. Common causes:
The requested scope falls outside the key's granted region for the verb
A write targets a scope outside the key's write region
A request acts on behalf of a principal that does not allow delegation
The operation targets a reserved principal or scope, such as updating the
systemprincipal or tombstoning the root scope/
{
"message": "the root scope `/` cannot be tombstoned"
}404 Not Found
The requested resource does not exist.
{
"message": "document with id 0197d8f2 does not exist"
}409 Conflict
A resource with the same identifier already exists, or an idempotency conflict occurred:
Duplicate Context id on create
Duplicate principal
display_nameon create (when no matchingexternal_idcreate-or-get applies)Same
Idempotency-Keywith a different request body on/factsor/facts/batchDuplicate idempotency request while the first is still in flight
A configured limit is exceeded (
ScopeLimitExceeded), such as the scope depth or the number of labels on a factA connector or binding already exists, a connector is still in use, or a binding action is not valid from its current status
{
"message": "context already exists"
}413 Payload Too Large
The uploaded file exceeds the upload limit. The limit is set for the whole deployment, not for each Context, and is 4 GiB by default.
415 Unsupported Media Type
The uploaded file has a media type that the server cannot ingest.
{
"message": "unsupported media type: application/x-msdownload"
}422 Unprocessable Entity
The request is syntactically valid but semantically invalid. Examples are an invalid Context id, namespace or database name (including the reserved spectron namespace), a malformed structured fact entry, or an uncertainty flag that names no subject.
{
"message": "invalid namespace: spectron"
}429 Too Many Requests
The Context is blocked from LLM-backed work, or a per-minute rate limit was hit.
Token budget: returns 429 when enforcement_blocked is true on the Context (for example, organisation credit enforcement on SurrealDB Cloud). A soft token_limit breach alone does not reject while enforcement_blocked is false - usage continues under pay-as-you-go.
Applies to LLM-backed paths including /chat, /facts?infer=full, /reflect, and /consolidate - not to read-only cache hits or direct lookups.
{
"message": "token limit exceeded for context"
}500 Internal Server Error
An unexpected error occurred server-side. The message does not include internal details, which are written to the server log instead.
{
"message": "unknown error"
}501 Not Implemented
The request needs connectors, but connectors are disabled on this deployment or no connector host is configured.
502 Bad Gateway
A call to the connector host, or to the customer data source beyond it, failed. The message carries a reason code, never a source credential.
503 Service Unavailable
The server is temporarily unable to handle requests. This covers a SurrealDB connection issue or startup, and also load shedding: a serving pod above its in-flight request cap sheds new work rather than queueing it. Every end-user operation can answer 503; GET /health is exempt so a shedding pod still reports its own state.
A shed response carries a Retry-After header in delta-seconds alongside the usual error body. Every other 503 from the server carries the header as well.
HTTP/1.1 503 Service Unavailable
Retry-After: 4Wait the larger of your own backoff and the advertised delay before retrying. Jittered backoff alone typically starts below Retry-After, so a client that ignores the header can exhaust its attempts inside the window the server asked it to sit out, and every one of those attempts lands on a pod that is still saturated.
Only the delta-seconds form is emitted. A client that encounters the HTTP-date form should fall back to plain backoff.
Browser clients can read this header because the CORS policy names it in Access-Control-Expose-Headers. Retry-After is not one of the CORS-safelisted response headers, so a caller reading it from script in a different origin without that exposure would see null and quietly lose the delay.
SDK exceptions
Python SDK
| Exception | HTTP status | When |
|---|---|---|
MemoryAuthError | 401 | Missing or invalid API key |
MemoryScopeError | 403 | Scope floor or principal rejects the call |
MemoryNotFoundError | 404 | Resource does not exist |
MemoryAPIError | Other non-2xx | Generic API failure (includes 400, 409, 429, 5xx) |
from surrealdb.memory import MemoryNotFoundError, MemoryAPIError
try:
doc = await client.documents.get("document:nonexistent")
except MemoryNotFoundError as e:
print(f"Document not found: {e.message}")
except MemoryAPIError as e:
if e.status_code == 429:
print(f"Rate limit exceeded: {e.body}")See the SDK error sections for Python and JavaScript for the full hierarchy and retry behaviour.
JavaScript SDK
| Error class | HTTP status | When |
|---|---|---|
AuthError | 401 | Missing or invalid API key |
ScopeError | 403 | Scope floor or principal rejection |
NotFoundError | 404 | Resource does not exist |
ValidationError | 400, 422 | Invalid request payload |
RateLimitError | 429 | Token or rate limit exceeded |
ServerError | 5xx | Server-side failure |
ConnectionError | - | Network failure or timeout |
StreamError | - | A streamed response failed part of the way through |
CancelledError | - | The caller aborted the request through its signal |
import { NotFoundError, RateLimitError } from "@surrealdb/memory";
try {
const doc = await client.documents.get("document:nonexistent");
} catch (e) {
if (e instanceof NotFoundError) {
console.error("Document not found:", e.message);
} else if (e instanceof RateLimitError) {
console.error("Rate limit exceeded");
}
}Ingestion pipeline errors
Documents that fail during async processing do not return HTTP errors - they set the document status to "failed" and populate the error field:
{
"id": "document:0197d8f2...",
"status": "failed",
"error": "PDF extraction failed: file is encrypted and no password was provided.",
"processingStartedAt": "2026-05-12T14:22:11Z",
"processingCompletedAt": "2026-05-12T14:22:14Z"
}Poll GET /api/v1/{context_id}/documents/{id} to check document status. See Uploading documents for retry guidance.
MCP error handling
MCP tool failures use a different envelope from REST, but carry the same HTTP status semantics:
Operation failures →
isError: truetool result withstructuredContent.error.status(404, 403, 429, etc.). The JSON-RPC transport returns HTTP 200.Protocol faults → JSON-RPC
error(bad params, unknown tool).
Auth failures and missing Contexts are masked as 401, never 404, so unauthenticated callers cannot enumerate Context ids. See MCP tools.