Production checklist
This page lists what to check before your integration goes live. Each item says what to do and why.
Go-live checklist
Section titled “Go-live checklist”- Handle keys safely. Call Alvia from your server, keep keys in a secret manager and use a separate production project. See Keys.
- Test the memory policy. Check allowed facts, excluded words, paraphrases and mixed conversations before the first production write. See Writes.
- Prove isolation. Show that one project’s key cannot reach another’s memories and that your server chooses each user’s scope. See Prove isolation and recovery behavior.
- Retry only what can succeed. Back off with jitter and bound your attempts. See Retries.
- Set timeouts and fallbacks. Decide what the application does without context. See Latency and capacity.
- Monitor. Log each call’s outcome and watch your allowances. See Monitoring.
- Follow the key rules. Call Alvia from your server only, keep keys in a secret manager, and use one key for each service and environment. See Keep keys safe in Authentication.
- Use separate projects for production and testing. Test data then never reaches your production context. See Projects and scope.
- Rotate before expiry. Choose a key lifetime that fits your deployment and replace it before it expires. For keys without an expiration, set your own rotation schedule. See Rotate a key in Authentication.
Writes
Section titled “Writes”Before the first production write or backfill, run the policy checks in
Filter memories. Use an allowed fact as a positive control,
then test restricted words, meaning paraphrases and mixed conversations. Verify the policy
against the formed memories: a 202 confirms acceptance, and the memories show what was kept.
- Send
occurred_atwhen the information is not new. Imported records, delayed queues and backfills must carry the time the information was stated or observed. See Point-in-Time Reasoning and change over time. - Send times with the user’s offset. Relative dates such as “yesterday” resolve on that day.
- Write in the background. A write returns
202before formation runs, so your user does not need to wait for it. - Read a new memory after formation. Formation finishes in less than a minute, so a new memory is readable shortly after the write.
- Set a project memory policy. Exclude sensitive information by keyword and meaning before importing conversations. Remove secrets, system prompts and tool output in your application too. See Filter memories.
- Use context for prompts, and recall when you need an ID. See Read memories.
- Send scope on every read that serves one user. A read without scope covers the whole project.
- Treat “no context” as a normal result. Context returns
404and recall returns"status": "not_found"when Alvia has no context for the question right now; a later call can return one. Your application should still respond. - Set a timeout on every call. Decide what your application does without context.
Retries
Section titled “Retries”Retry a request only when a later attempt can succeed:
| Status | Retry | What to do |
|---|---|---|
400, 401, 404, 409, 413, 415 |
No | Fix the request, the key or the account state first. |
429 with a reason |
Not until the reset | A monthly allowance is used up. Retry-After gives the seconds until it resets. |
429 without a reason |
Yes | Wait for the seconds in Retry-After, then retry. |
502, 503, 504 |
Yes | Wait for Retry-After when present; otherwise back off. |
500 |
A few times | Back off. If it continues, report it with the response body. |
| Network error or timeout | Yes | Back off. |
Back off with growing, random waits: for example 1, 2, 4 and 8 seconds, each with up to a second of random jitter, then stop. Jitter keeps many clients from retrying at the same moment.
import randomimport timeimport requests
RETRY_STATUSES = {429, 500, 502, 503, 504}
def call_alvia(method: str, url: str, **kwargs) -> requests.Response: """Sends a request, retrying responses a later attempt can fix.""" for attempt in range(5): try: response = requests.request( method, url, timeout=30, **kwargs ) except (requests.ConnectionError, requests.Timeout): if attempt == 4: raise else: status = response.status_code reason = None if status == 429: try: problem = response.json() if isinstance(problem, dict): reason = problem.get("reason") except ValueError: pass # An intermediary may return HTML. used_up = isinstance(reason, str) and reason in { "monthly_read_quota_reached", "monthly_write_quota_reached", } if ( status not in RETRY_STATUSES or used_up or attempt == 4 ): return response try: wait = float(response.headers.get("Retry-After", "")) except ValueError: wait = None # Absent or an HTTP date: back off. if wait is not None: time.sleep(max(wait, 0)) continue time.sleep(2**attempt + random.random()) raise RuntimeError("unreachable")const RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
/** Sends a request, retrying responses a later attempt can fix. */async function callAlvia(url, init) { for (let attempt = 0; attempt < 5; attempt++) { let response; try { response = await fetch(url, { ...init, signal: AbortSignal.timeout(30_000), }); } catch (error) { if (attempt === 4) throw error; await sleep(2 ** attempt + Math.random()); continue; } const status = response.status; const problem = status === 429 ? await response.clone().json().catch(() => null) : null; const usedUp = [ "monthly_read_quota_reached", "monthly_write_quota_reached", ].includes(problem?.reason); if (!RETRY_STATUSES.has(status) || usedUp || attempt === 4) { return response; } const header = response.headers.get("Retry-After"); const retryAfter = header === null ? NaN : Number(header); const backoff = 2 ** attempt + Math.random(); // Absent or an HTTP date: back off. await sleep(Number.isFinite(retryAfter) ? retryAfter : backoff); }}import ( "bytes" "encoding/json" "io" "math/rand/v2" "net/http" "strconv" "time")
var retryStatuses = map[int]bool{ 429: true, 500: true, 502: true, 503: true, 504: true,}
var client = &http.Client{Timeout: 30 * time.Second}
// callAlvia sends a request, retrying responses a later attempt can// fix. newRequest builds the request again for each attempt, because// a sent body is used up.func callAlvia( newRequest func() (*http.Request, error),) (*http.Response, error) { for attempt := 0; ; attempt++ { req, err := newRequest() if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { if attempt == 4 { return nil, err } time.Sleep(backoff(attempt)) continue } final := !retryStatuses[resp.StatusCode] || attempt == 4 if final || usedUp(resp) { return resp, nil } wait := backoff(attempt) retryAfter := resp.Header.Get("Retry-After") if s, err := strconv.ParseFloat(retryAfter, 64); err == nil { wait = time.Duration(s * float64(time.Second)) } resp.Body.Close() time.Sleep(wait) }}
// usedUp reports whether a 429 has a reason: a used-up monthly// allowance, which no retry fixes before the reset. It leaves the// body readable.func usedUp(resp *http.Response) bool { if resp.StatusCode != http.StatusTooManyRequests { return false } body, err := io.ReadAll(resp.Body) resp.Body.Close() resp.Body = io.NopCloser(bytes.NewReader(body)) var problem struct { Reason string `json:"reason"` } if err != nil || json.Unmarshal(body, &problem) != nil { return false } return problem.Reason != ""}
// backoff waits 1, 2, 4 and 8 seconds, plus up to a second at random.func backoff(attempt int) time.Duration { seconds := float64(int(1)<<attempt) + rand.Float64() return time.Duration(seconds * float64(time.Second))}using System.Net;using System.Net.Http.Json;using System.Text.Json;
static class Alvia{ static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(30), };
static readonly HashSet<HttpStatusCode> RetryStatuses = [ HttpStatusCode.TooManyRequests, HttpStatusCode.InternalServerError, HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout, ];
// Sends a request, retrying responses a later attempt can fix. // newRequest builds the request again for each attempt, because // a request can be sent only once. public static async Task<HttpResponseMessage> CallAlvia( Func<HttpRequestMessage> newRequest) { for (var attempt = 0; ; attempt++) { HttpResponseMessage response; try { response = await Http.SendAsync(newRequest()); } catch (Exception e) when (attempt < 4 && e is HttpRequestException or TaskCanceledException) { await Task.Delay(Backoff(attempt)); continue; } var final = !RetryStatuses.Contains(response.StatusCode) || attempt == 4; if (final || await UsedUp(response)) { return response; } var retryAfter = response.Headers.RetryAfter?.Delta; response.Dispose(); await Task.Delay(retryAfter ?? Backoff(attempt)); } }
// A 429 with a reason is a used-up monthly allowance, which no // retry fixes before the reset. static async Task<bool> UsedUp(HttpResponseMessage response) { if (response.StatusCode != HttpStatusCode.TooManyRequests) { return false; } try { var body = await response.Content .ReadFromJsonAsync<JsonElement>(); return body.ValueKind == JsonValueKind.Object && body.TryGetProperty("reason", out var reason) && reason.ValueKind == JsonValueKind.String && reason.GetString() is "monthly_read_quota_reached" or "monthly_write_quota_reached"; } catch (JsonException) { return false; // An intermediary may return HTML. } }
// Waits 1, 2, 4 and 8 seconds, plus up to a second at random. static TimeSpan Backoff(int attempt) => TimeSpan.FromSeconds( Math.Pow(2, attempt) + Random.Shared.NextDouble());}Retry only requests that failed: network errors, 5xx responses and 429 responses without a
reason (after the wait in Retry-After). A 202 means Alvia accepted the write, so do not send
it again. When you retry, send the same content, scope and occurred_at. Each accepted request
uses writes. See Retries in Store memories.
Latency and capacity
Section titled “Latency and capacity”Timeouts and fallbacks
Section titled “Timeouts and fallbacks”Choose each timeout from the time your application can afford to wait. Measure context, recall and search separately under representative load: context writes an answer, while recall and search return memory records. When facts conflict, rely on resolved context rather than the first search hit.
Decide each fallback before going live. An interactive support screen can start the context read alongside the CRM lookup and show verified CRM data if the context deadline passes. A refund workflow should hold a consequential case for review when required context is unavailable. Cancel work your application no longer needs, and bound both request attempts and total retry time.
Time to readable
Section titled “Time to readable”A write’s response time measures acceptance, not formation. Measure separately how long a synthetic accepted write takes to become readable. Do not poll on every user keystroke or block a conversation while formation runs: an application that must acknowledge a new preference at once should use its current interaction state until formation finishes.
Shared capacity
Section titled “Shared capacity”Rate limits and monthly allowances apply to the whole account, across every project and key, so
pace batch jobs to leave room for live traffic and plan test projects into the same capacity.
Budget reads per interaction, and slow down before RateLimit-Remaining
reaches zero. See Rate limits.
Backfills
Section titled “Backfills”- Measure expected concurrent interactions, reads per interaction and write volume.
- Compare sustained request rates and bursts with Rate limits, and leave capacity for live traffic.
- Run a bounded worker queue for imports, with backpressure and a way to stop a backfill.
- Preserve each conversation’s original date and test the policy before a bulk submission.
- Track accepted submissions separately from memories formed; accepted text can form no memory.
Import each conversation once. Keep a submission record in your application, so a restarted backfill resumes where it stopped and sends nothing twice.
Monitoring
Section titled “Monitoring”- Log the operation, duration, HTTP status, error code and reason. Use an opaque correlation ID. Do not log keys, raw request bodies, generated context or customer scopes containing PII.
- Count misses separately from failures. Context
404, recallnot_foundand empty lists can be valid results. Alert separately on authentication failures,429and5xxresponses. - Measure end-to-end latency in your application.
Server-Timingon REST memory calls reports Alvia’s processing time; measure the full round trip in your application. - Track usage.
Alvia-Usageon those calls reports the reads and writes the request used, and facts formed later add to the total usage the dashboard shows. - Review calls in the activity log. A project’s Activity page in the dashboard lists each API-key call with its operation, status and scope, and the query text and ranked results of reads, for 90 days. See Security.
- Watch remaining allowance and stored-memory capacity in the dashboard. See Limits and allowances.
- Tolerate additive response changes. Ignore fields you do not use and handle unfamiliar categories and reasons. See Errors.
Prove isolation and recovery behavior
Section titled “Prove isolation and recovery behavior”Before production, use invented data in separate projects A and B. Store a distinct fact in each, then verify that A’s key cannot retrieve, forget or give feedback on B’s memory ID. Test your application’s user A and user B sessions too: the server must select their scope, regardless of scope values supplied by a client. Keys within one project do not provide user-level access control.
Test missing context, expired credentials, a 429, a timeout after an accepted write and a
restarted import job. Verify the intended fallback and bounded retries in each case. For a
forget test, capture the memory ID first, forget it, then verify get, list, recall, search and
context as described in Manage memories. Forgetting is
permanent, so confirm before you forget. Keep the project’s retained-data and erasure procedure in
your operational review. See Security.