> For the complete documentation index, see [llms.txt](https://docs.gitloom.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.gitloom.cloud/documentation/content/limits.md).

# Limits and behaviour

This page is the one that saves you an afternoon. It documents what GitLoom actually does today, including the parts that are not finished. Where a number is measured, it says so and against what. Where something is not enforced yet, it says that too, rather than describing an intention as a behaviour.

### Writes are asynchronous

`POST /v1/memories` returns **`202 Accepted`**. The memory does not exist yet.

```json
{"id":"3f2b9e01-7c44-4d1a-9f6e-0a1b2c3d4e5f","namespace":"user-8213","status":"accepted"}
```

`202` rather than `200` because extraction is several model calls against a conversation. Claiming `200` would make the next read look broken.

What happens between the `202` and the memory existing:

1. The request is validated — credential, namespace exists, at least one message — and enqueued. **This is where a rejection happens**, while you are still listening.
2. A worker picks it up, fetches the namespace's repository, and runs extraction, cue-writing, relation-linking, and reconciliation.
3. New and rewritten memories are committed to git, embedded, packed, and pushed back to object storage with a compare-and-swap.
4. The namespace's recorded head advances. From that instant, retrieval returns the new memories.

#### How long that takes

There is no published SLA, and you should not build a UI that depends on a specific figure. What is measurable:

* A single extraction call against a one-line conversation completed in **1.3s** through the Bedrock adapter.
* A realistic session runs four prompt stages, some of which fan out across the extracted facts. Expect **seconds for a short conversation, tens of seconds for a long one**.
* The worker's hard timeout is **15 minutes**. Anything approaching that is a bug, not a slow day.

#### There is no way to poll for completion

This is the sharpest edge in the current API, so it is worth stating plainly:

* The `id` in the `202` is the **queue message id**. It is not a memory id, and there is no endpoint that accepts it.
* There is no `GET /v1/memories/{id}`, no job status route, and no webhook.
* The only observable signal that ingestion landed is that `GET /v1/retrieve` starts returning the new material.

**What to do instead.** Do not write-then-immediately-read in the same user turn and expect the memory back. Write at the end of a turn and retrieve at the start of the next one; by then, ingestion has almost always finished. If you genuinely need to confirm, poll `/v1/retrieve` with a query you know the new content should match, with a backoff, and give up gracefully — retrieval is cheap, but it is not free.

#### Ordering and duplicate suppression

* **Writes to one namespace are serialised.** The queue's message group is the `account/namespace` pair, so one write to a given memory runs at a time. That is required for correctness: reconciliation decides whether a new fact rewrites an existing file, so two concurrent writers rebuilding the same repository would discard one another's work wholesale rather than merge it.
* **Different namespaces run in parallel**, bounded by worker concurrency (below).
* **Identical bodies within a five-minute window are deduplicated.** The queue is content-addressed for deduplication, so resending the exact same request body twice in quick succession returns `202` both times and ingests **once**. This absorbs an at-least-once caller; it also means a genuine retry of an identical payload is silently a no-op. Vary `session_id` if you mean two distinct writes.
* **Failed messages are retried up to three times**, then moved to a dead-letter queue where they are held for 14 days and alarmed on. A message reaching the DLQ is a lost write, and it is treated as a correctness alarm rather than a backlog one.

#### A partial failure that is not visible to you

If extraction succeeds but embedding fails, the memories are **committed to git anyway** and the failure is logged. Those memories are retrievable immediately by the lexical and graph arms, but not by semantic search, until the next successful write to that namespace fills the missing vectors in. Failing the whole job instead would re-run — and re-charge for — extraction that already worked.

The practical symptom: a memory that a keyword query finds but a paraphrased question does not, which starts working after the user's next session is ingested.

### Retrieval latency

`GET /v1/retrieve` makes no model call except the query embedding.

Measured against the deployed stack over 200 authenticated requests:

|                                               | p50        | p99        | max    |
| --------------------------------------------- | ---------- | ---------- | ------ |
| **Server-side retrieval** (`RetrievalMillis`) | **109 ms** | **145 ms** | 152 ms |
| Client end-to-end, India → `ap-south-1`       | 211 ms     | 383 ms     | 765 ms |

Read that table carefully, because the two rows mean different things:

* **109 / 145 ms is server-side**: the time inside the retrieval function. It is the number the internal p99 alarm watches, against a 200 ms budget.
* **211 / 383 ms is what a client actually waits.** The difference is network. If you are further from `ap-south-1` than India is, your number is larger, and no amount of work on our side changes that. GitLoom runs in one region today.

**Roughly 100 ms of the server-side figure is the query embedding** — a network round trip to Bedrock, not search. For comparison, before an embedder was wired the same measurement was p50 7.4 ms / p99 19.6 ms. So:

* Search itself costs single-digit milliseconds.
* The p99 budget is now spent almost entirely on one round trip, and the margin is 1.4x rather than the tenfold it used to be.
* The lexical and semantic arms run concurrently, so the embedding is not *additive* with lexical time — it dominates it.

`vector_ms`, `lexical_ms` and `graph_ms` are returned on every response so you can see this yourself rather than take our word for it.

#### Cold starts and cache behaviour

* `cold_start: true` marks the first request served by a fresh container. A cold start has been observed at **167 ms** of function duration versus a 9 ms warm p50, plus the cost of fetching the namespace's repository.
* A container keeps up to **four namespaces** warm on local disk, evicting the least recently used. A fifth namespace on the same container pays a fetch.
* Freshness is not a guess: every request does a metadata read and compares the warm copy's commit against the namespace's recorded head. A stale copy is discarded. So **once ingestion records a write, the next read sees it** — there is no propagation window to design around, and a deletion cannot linger in a warm cache.

#### Retrieval defaults

|                     |                                |
| ------------------- | ------------------------------ |
| `limit` default     | 24 hits                        |
| Graph expansion     | 1 hop, from the top 6 hits     |
| Expired `incidents` | excluded                       |
| Empty result        | `hits` is **absent**, not `[]` |
| Namespace listing   | first 200 namespaces           |

### Embeddings

|                  |                                                                      |
| ---------------- | -------------------------------------------------------------------- |
| Model            | Amazon Titan Text Embeddings **v2** (`amazon.titan-embed-text-v2:0`) |
| Dimensions       | **1024**                                                             |
| Normalised       | yes                                                                  |
| Symmetric        | yes — queries and stored text are embedded identically               |
| What is embedded | **cues**, not memory bodies                                          |
| Region           | `ap-south-1`, via Bedrock                                            |

Why Titan and not Cohere, which would batch better: Cohere on Bedrock is a Marketplace model requiring an account-level subscription accepted in the console — IAM permissions alone do not unlock it. Titan is Amazon-owned and works with nothing but `bedrock:InvokeModel`. Both produce 1024 dimensions, so the stored vector width does not change if that decision is revisited.

Two consequences worth knowing:

* **Your text is sent to Amazon Bedrock** in `ap-south-1`, for both embedding and extraction. Extraction uses Claude Haiku 4.5 through Bedrock's Converse API. If your data cannot leave a boundary that excludes this, GitLoom's hosted service is not the right deployment.
* **Semantic search scans linearly** in the number of stored vectors. The published latency figures were measured on a 240-memory fixture, and they do not extrapolate to a very large namespace on their own. One namespace per end user keeps each index small, which is a large part of why the model is shaped that way.

### Quotas

**Nothing is metered or capped per account today.** Self-serve signup is open, and there is no per-account request limit, no memory count limit, no namespace count limit, and no spend limit. This is a known gap, and it is stated here rather than implied away.

#### What *is* enforced right now

These are real, deployed limits. They are infrastructure ceilings, not per-account quotas, which means they are shared across every account:

| limit                                        | value                  | what you see when you hit it                                     |
| -------------------------------------------- | ---------------------- | ---------------------------------------------------------------- |
| Concurrent retrievals, whole platform        | 20                     | Lambda throttles; the gateway returns `5xx`. Retry with backoff. |
| Concurrent ingestion workers, whole platform | 5                      | Nothing — the queue absorbs it. Your write takes longer to land. |
| Ingestion worker timeout                     | 15 min                 | The message is retried, then dead-lettered.                      |
| Queue retention                              | 4 days                 | A message not processed in 4 days is dropped.                    |
| Namespace name                               | `[a-z0-9-]`, ≤64 chars | `400 invalid_namespace`                                          |
| Namespaces returned by `GET /v1/namespaces`  | 200                    | The list is truncated silently.                                  |
| Credential cache at the gateway              | 30 s                   | See below.                                                       |

**Revocation takes up to 30 seconds.** Authorization results are cached for 30 seconds so that an agent calling with the same key every turn does not pay a lookup each time. A revoked key therefore keeps working for up to half a minute. Treat revocation as "stops within a minute", not "stops instantly", and rotate anything genuinely compromised with that in mind.

#### What happens when quotas do arrive

They will be enforced at the API edge, and the contract is fixed now so that clients written today do not need rewriting:

```http
HTTP/1.1 429 Too Many Requests
retry-after: 30
content-type: application/json
```

```json
{"error":{"code":"quota_exceeded","message":"account quota exceeded; retry after 30s"}}
```

* Rate limits will return `429` with `retry-after` in seconds. **Retry with exponential backoff and jitter.**
* Volume limits (memories stored, ingestion beyond an allowance) will return `402 payment_required` on `POST /v1/memories`, and **reads will keep working**. Losing access to memories you have already stored because a card expired is not a behaviour we want to ship.
* Neither status code is returned by the API today. If you are writing a client now, handle both anyway; that is the whole point of publishing the shape early.

### Errors

The error body has **two shapes**, which is an inconsistency you have to code around today:

Everything except retrieval:

```json
{"error":{"code":"namespace_not_found","message":"create namespace \"user-8213\" before writing to it"}}
```

`GET /v1/retrieve`:

```json
{"error":"namespace not found","namespace":"user-8213"}
```

Branch on `code` where it exists and on the HTTP status everywhere. The message text is prose and may be reworded; the codes are stable.

| status | code                  | meaning                                           |
| ------ | --------------------- | ------------------------------------------------- |
| `400`  | `invalid_body`        | body missing or not valid JSON                    |
| `400`  | `invalid_namespace`   | namespace name outside `[a-z0-9-]{1,64}`          |
| `400`  | `no_messages`         | `messages` was empty                              |
| `400`  | `invalid_env`         | key `env` was not `live` or `test`                |
| `401`  | —                     | missing, malformed, expired or revoked credential |
| `403`  | `dashboard_only`      | key management attempted with an API key          |
| `404`  | `namespace_not_found` | write to a namespace that was never created       |
| `404`  | `not_found`           | no such key, or no such route                     |
| `500`  | `internal`            | logged our side; safe to retry                    |

A `401` is deliberately uninformative. Distinguishing "no such key" from "wrong secret" tells an attacker which half of a guess was right, so both fail identically — and in constant time.

### Environments: `live` and `test` are not isolated

A key carries an environment in its prefix, `gl_live_` or `gl_test_`, and `GET /v1/whoami` reports it. **The environment is not currently a data boundary.** A `gl_test_` key reads and writes exactly the same namespaces as a `gl_live_` key on the same account.

The prefix is useful today for two real things: a production credential pasted into a test config is obvious on sight, and secret scanners can match it. It is not a sandbox. If you need one, use a separate namespace — that *is* a hard boundary — or a separate account.

### Browsers

The API's CORS policy allows **`GET` and `OPTIONS` only**, from the dashboard's origins. A browser cannot `POST /v1/memories`, `POST /v1/namespaces`, or manage keys.

That is a limitation, but the security advice would be identical without it: an API key is account-wide read and write, so it belongs on a server. Call GitLoom from your backend, and let your backend decide what a browser may ask for.

### What GitLoom does not do yet

Listed so you can plan around them rather than discover them:

* **No delete.** There is no endpoint to remove a memory or a namespace. Nothing in the API satisfies a GDPR erasure request today; that has to be done by an operator.
* **No update or direct write.** You cannot author a memory yourself. Everything goes through conversation ingestion.
* **No listing or reading by path.** `GET /v1/retrieve` is the only way to see stored content, and it returns a 240-character snippet rather than the full file.
* **No usage or billing endpoint.** Stored memory counts are recorded internally but are not exposed.
* **No pagination anywhere.** Listings are capped and truncated silently.
* **One region.** `ap-south-1`. There is no data-residency option.
* **No webhooks or events.**

### Reproducing these numbers

The latency figures come from the deployed stack, driven over 200–250 authenticated requests against a 240-memory fixture, and read back from the same `Gitloom/Retrieval` metrics the production p99 alarm watches. Ingestion timings come from the worker's own logs. Nothing on this page is an estimate presented as a measurement; where a figure is a range rather than a number, it is written as one.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.gitloom.cloud/documentation/content/limits.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
