# Reliability

SigWise has two ways to get a verdict, and they fail differently. Pick the one
that matches how much a late or missing answer costs you.

| | Asynchronous (default) | Inline (`"wait": true`) |
|---|---|---|
| Your request returns | `202` as soon as the events are recorded | `200` with the verdict, after the analysis |
| If the analyzer is slow or down | The analysis is retried in the background with exponential backoff. Your request is not affected | Your request waits, then fails |
| Best for | Scores that build over time, alerts, search | A decision you must make before something is published or paid |

## Inline checks: decide what a failure means

An inline check puts the analyzer in your request path, so decide in advance
what your product does when it does not answer. This is a product decision with
no universal answer:

| Situation | Usual choice | Why |
|---|---|---|
| Publishing a message or listing | **Fail open**: publish, then score again in the background | A short outage should not stop your users from talking |
| Releasing a payout, changing a bank detail, sending an invite to many users | **Fail closed**: hold for review | The cost of letting a bad action through is higher than a delay |
| First message from a brand-new account | **Hold** until the verdict arrives, with a short budget | Most of the risk sits here |

Give the call a budget shorter than the server's limit (the server gives up on
any request after 30 seconds), and treat running out of budget as a failure:

Node.js:

```ts
type Decision = "publish" | "hold" | "block";

const withBudget = <T>(p: Promise<T>, ms: number) =>
  Promise.race([
    p,
    new Promise<T>((_, reject) => setTimeout(() => reject(new Error("budget")), ms)),
  ]);

async function checkMessage(userId: string, text: string): Promise<Decision> {
  try {
    const verdict = await withBudget(
      sigwise.events.ingest(userId, {
        wait: true,
        signals: ["is_scammer"],
        events: [{ type: "message", content: text }],
      }),
      3000,
    );
    const p = "answers" in verdict ? (verdict.answers[0]?.noul ?? 0) : 0;
    if (p >= 0.9) return "block";
    if (p >= 0.6) return "hold";
    return "publish";
  } catch {
    // The events may or may not have been recorded. Publish now (fail open)
    // and let the background analysis and your rules catch it afterwards.
    return "publish";
  }
}
```

Python:

```python
def check_message(user_id: str, text: str) -> str:
    try:
        verdict = sigwise.events.ingest(
            user_id,
            wait=True,
            signals=["is_scammer"],
            events=[{"type": "message", "content": text}],
        )
        answers = verdict.get("answers") or []
        p = answers[0].get("noul", 0) if answers else 0
        if p >= 0.9:
            return "block"
        if p >= 0.6:
            return "hold"
        return "publish"
    except Exception:
        # Fail open. Set a request timeout on the client (see your SDK's
        # README) so this does not wait for the server's 30 seconds.
        return "publish"
```

Choose the thresholds from your own test, not from this example. See
[Test SigWise on your own data](https://sigwise.ai/docs/guides/evaluating.md).

## What each failure looks like

| Response | Meaning | What to do |
|---|---|---|
| `200` with `analyzed: false` | No signal matched, so nothing was scored | Treat as no verdict. The events are recorded |
| `402` | The prepaid balance is empty | Top up. Decide whether you fail open meanwhile |
| `429` | Too many requests or too many inline analyses at once | Wait for `Retry-After`. Fail open or hold, as above |
| `502` | The analysis provider failed | Fail open or closed, as above |
| `503` `analyzer_busy` | The provider is throttling the request | After `Retry-After`, ask for an analysis again or read the answers later |
| Timeout on your side | You do not know whether the events were recorded | Retry with the same `Idempotency-Key` |

Whether the events were recorded on a `502` or `503` depends on your
[retention policy](https://sigwise.ai/docs/guides/data-retention.md). With the default, they were, and you
should not send them again. With `after_analysis`, inline requests are never
written, so nothing was recorded and you should resend. Sending the same
`Idempotency-Key` makes a resend safe either way. See
[Errors and retries](https://sigwise.ai/docs/guides/errors.md).

## Asynchronous analyses

If an analysis fails, it is retried with exponential backoff, capped at five
minutes between attempts, up to a fixed number of attempts, and then marked
failed. If the provider is throttling, the analysis is put back until the time
the provider asked for, without using up an attempt.

An empty balance is not retried, because retrying cannot succeed until you top
up. The analysis is failed straight away. After you top up, the next event for
the object schedules a new analysis, or you can request one with
`POST /v1/objects/{id}/analyze` ([API reference](https://sigwise.ai/docs/reference.md)).

## Analysis providers

SigWise can run on more than one analysis provider (see
[Sub-processors](https://sigwise.ai/docs/trust/subprocessors.md)), and the provider for an account is set
by SigWise. There is no automatic failover from one provider to another for
a single request today. If a provider is having a long outage, ask for your
account to be moved to the other one.

## Measure latency yourself

Inline latency is dominated by the provider's response, which varies with how
much history an object has and how many signals you score. Do not rely on a
single number. Every inline response includes `latency_ms`, so log it for a
week of real traffic and look at the 50th and 99th percentiles before you pick
your budget. Scoring only the signals you need with `signals` keeps it lower.

## Health endpoints

The API exposes `GET /healthz` (the process is up) and `GET /readyz` (the
database is reachable). Use `/readyz` for load balancer checks.
