GitLoom

Limits and behaviour

What is asynchronous, how fast retrieval actually is, what the vectors are, and what is not enforced yet.

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.

{"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:

There is no way to poll for completion

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

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

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.

Write size

One POST /v1/memories carries at most 200 turns and 200,000 characters of conversation, in a body of at most 256 KiB. On the Free plan a write is capped at 20,000 characters; a longer one is refused with 413 write_too_large before it is queued or counted. Split a long conversation into several writes.

Retrieval latency

A plain retrieval (the default: mode=raw, no rank=jev) makes no model call except the query embedding, which Titan computes in ap-south-1.

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:

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:

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

Retrieval defaults

limit default 24 memories, max 200
Graph expansion 1 hop, from the top 6 memories; context=0 turns it off
Expired incidents excluded
Empty result memories is []
mode=summary one call to a fast model (Claude Haiku 4.5 by default); metered as a chat
mode=agentic up to 6 model turns and 10 tool calls inside a 24-second budget; metered as a chat
rank=jev one call to TypeSafe's Jev ranking model, outside AWS, only when asked for; metered as a chat. If it cannot rank, the memories come back in lane order with rank_fallback: true
model=sonnet Claude Sonnet 4.6 reads the memories instead of the mode's default; metered as a chat
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:

Quotas

Per-account quotas are enforced. Every account has a plan — free if nothing was ever chosen — and every write, retrieval, playground chat, and stored-memory count is metered against that plan's monthly limits. See Plans and pricing for the numbers themselves; this section is about the mechanics.

{"error":{"code":"quota_exceeded","message":"monthly limit reached: 500 writes on the free plan; it resets on 2026-11-01 at 00:00 UTC, or upgrade at https://app.gitloom.cloud"}}
{"error":{"code":"balance_exhausted","message":"the wallet is empty; recharge from the dashboard's billing page"}}
{"error":{"code":"rate_limited","message":"rate limited: the free plan allows 60 reads per minute; retry in 12s"}}

A notification fires internally when a meter crosses 80% of its limit, so an account approaching a ceiling is not surprised by it.

Infrastructure ceilings, shared across every account

Separate from per-account quotas, these are platform-wide limits — they exist so no single account's traffic can starve every other account's, and they are not sized per plan:

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.
Recharge, one transaction ₹500 min, ₹1,00,000 max 400 below_minimum / 400 above_maximum

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.

Errors

Every error a handler returns has one shape:

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

Branch on code and on the HTTP status. The message text is prose and may be reworded; the codes are stable. The exceptions are a missing or refused credential, which API Gateway answers before any handler runs, with a body of only {"message":"Unauthorized"} or {"message":"Forbidden"}. The SDKs report both as code unauthorized.

status code meaning
400 invalid_body body missing or not valid JSON
400 invalid_namespace namespace name outside [a-z0-9-]{1,64}
400 missing_query q was empty
400 invalid_tier, invalid_path, invalid_mode, invalid_detail, invalid_min_score, invalid_date a retrieval parameter was refused
400 no_terms, invalid_term, no_skills, invalid_skill a vocabulary or skill write was refused
400 invalid_env key env was not live or test
401 — no credential: the gateway answers {"message":"Unauthorized"}
403 — a refused credential (malformed, expired, revoked, or a suspended account): the gateway answers {"message":"Forbidden"}
403 dashboard_only an API key listed or revoked keys, or asked for an account-wide key
403 scoped_key a namespace-scoped key tried to create a key
404 namespace_not_found a namespace that was never created
404 not_found no such key, memory, or route
413 too_many_terms, too_many_skills, payload_too_large a batch over its ceiling
502 model_failed a summary or agentic run produced nothing; retry with mode=raw
503 model_unavailable the answering model is not configured
500 internal logged our side; safe to retry

A refused credential is deliberately uninformative. Distinguishing "no such key" from "wrong secret" tells an attacker which half of a guess was right, so both fail identically, with the same 403 — 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

A browser can call the API only from the dashboard's own origins: the CORS policy allows them GET, POST, PATCH, DELETE and OPTIONS. From any other web origin the browser refuses the call, so call GitLoom from your server.

That is a limitation, but the security advice would be identical without it: an account-wide API key reads and writes every namespace, 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:

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.