# Network profiles and fault injection

Add latency and refuse a fraction of requests per endpoint with a network profile, from the control plane or at boot, and throttle Stripe charges with 429s.

A [network profile](https://www.rystic.ai/docs/concepts/glossary.md#network-profile) adds latency and refuses a fraction of requests, per endpoint, inside the [simulator](https://www.rystic.ai/docs/concepts/glossary.md#simulator)'s own HTTP path. Shaping is off by default and costs nothing until you turn it on. It needs the `faults` feature, which Pro and Enterprise include.

Only Kalshi ships a profile, off by default ([Kalshi → Production latency](https://www.rystic.ai/docs/simulators/kalshi/features/production-latency.md)). `RYSTIC_NETWORK=on` or `POST /_rystic/network` with `{"enabled":true}` turns it on. On any other simulator, `{"enabled":true}` alone answers `400 cannot enable shaping: no network profile installed`, so post a profile.

## The profile

```text
POST /_rystic/network
{
  "enabled": true,                     // optional; at least one of enabled / profile
  "profile": {                         // optional; replaces the active profile whole
    "id": "any-name",
    "default": SHAPE,                  // optional; any endpoint not listed below
    "endpoints": {
      "POST /v1/charges": SHAPE        // "METHOD template", exactly as GET /_rystic/routes lists it
    }
  }
}

SHAPE  = { "in": DELAY, "out": DELAY, "reject": REJECT }       // each optional
DELAY  = { "base_ms": 120, "jitter_ms": 40, "correlation": 0.3 }
       | { "quantiles_ms": { "p50": 90, "p90": 180, "p99": 400 }, "correlation": 0.3 }
REJECT = { "rate": 0.25, "status": 429, "body": { … } }        // or "body_text": "…"; "content_type" overrides
```

1. **Keys** are `METHOD /template` exactly as `GET /_rystic/routes` returns them: `POST /v1/charges`, `GET /v1/customers/{customer}`. An endpoint without a key uses `default`, or nothing.
2. **`in`** delays before the handler runs. **`out`** delays after state has changed, before the client sees the answer.
3. **A delay** is `base_ms` ± `jitter_ms`, or a `quantiles_ms` table (`p50` required, `p90` and `p99` optional). `correlation` in `[0,1)` makes consecutive delays drift together instead of jumping independently.
4. **`reject`** answers `rate` (0 to 1) of requests with `status` and exactly the `body` (JSON) or `body_text` you give. The type is `application/json` or `text/plain` unless `content_type` says otherwise.
5. **A rejected request** never reaches the handler, so no state moves. Delays still apply. It sets no other headers, so there is no `Retry-After`.
6. **Deterministic.** Delays and rejections draw from their own seeded stream. The same `seed-rng` and the same profile refuse the same requests in the same order on every run, and shaping never shifts the ids or values the vendor routes draw.
7. **Lifetime.** A posted profile survives `reset`, and `{"enabled":false}` switches it off and keeps it. A scenario that carries a profile, such as Kalshi's `rate_limited`, replaces it when it loads or at the times its tape names. The next `reset` or scenario brings back the profile from before the scenario. A network write made since, a post to `/_rystic/network` or a posted scenario file with a `network`, stays instead.
8. **Scope.** Network profiles never shape the control plane, the WebSocket feed or the replay routes.

`GET /_rystic/network` returns `{enabled, source, profile}`.

## Worked example: throttle Stripe charges with 429s

Start Stripe on a free port and keep the port in `PORT` (pull first, so `run` prints nothing but the JSON):

```bash
rystic pull stripe-twin
PORT=$(rystic run stripe-twin -d --name faults --port 0 --scenario design_studio --json | jq -r .data.port)
```

Refuse half of `POST /v1/charges` with a Stripe-shaped rate-limit error, and add 120 ± 40 ms on the way back:

```bash
curl -s -X POST "localhost:$PORT/_rystic/network" -H 'content-type: application/json' -d '{"enabled":true,"profile":{"id":"charges-throttled","endpoints":{"POST /v1/charges":{"reject":{"rate":0.5,"status":429,"body":{"error":{"type":"invalid_request_error","code":"rate_limit","message":"Request rate limit exceeded."}}},"out":{"base_ms":120,"jitter_ms":40}}}}}'
curl -s "localhost:$PORT/_rystic/network" | jq -c '{enabled, source}'
# -> {"enabled":true,"source":"control"}
```

Send ten charges and count the answers by status:

```bash
for i in $(seq 10); do curl -s -o /dev/null -w '%{http_code}\n' "localhost:$PORT/v1/charges" -u sk_test_twin: -d amount=2000 -d currency=usd -d source=tok_visa; done | sort | uniq -c | awk '{print $2, $1}'
# -> 200 6
# -> 429 4
```

A rate of `0.5` is a draw per request, not every other one, and the same six pass on every run. Only the `200`s created charges. Point your client at `localhost:$PORT` and check that it backs off and retries.

Then switch shaping off, since `reset` keeps a posted profile, and wipe the charges:

```bash
curl -s -X POST "localhost:$PORT/_rystic/network" -d '{"enabled":false}'
curl -s -X POST "localhost:$PORT/_rystic/reset"
curl -s "localhost:$PORT/_rystic/network" | jq -c '{enabled, source}'
# -> {"enabled":false,"source":"control"}
rystic stop faults
```

A `503` brownout is the same shape with an HTML page: `"reject": {"rate": 1, "status": 503, "body_text": "<html>…</html>", "content_type": "text/html"}`.

## In the state UI

The [state UI](https://www.rystic.ai/docs/concepts/observing.md#the-state-ui)'s **Network** card edits the installed profile while the simulator runs. Install one to edit, here a quantile table for Stripe's customer list, then open `http://localhost:$PORT/_rystic/ui` in a browser:

```bash
PORT=$(rystic run stripe-twin -d --name netui --port 0 --json | jq -r .data.port)
curl -s -X POST "localhost:$PORT/_rystic/network" -d '{"profile":{"id":"slow-customers","endpoints":{"GET /v1/customers":{"out":{"quantiles_ms":{"p50":100,"p90":250,"p99":600}}}}}}'
curl -s "localhost:$PORT/_rystic/network" | jq -c '{enabled, source, id: .profile.id}'
# -> {"enabled":false,"source":"control","id":"slow-customers"}
```

1. **shaping ON** / **shaping OFF** switches shaping, as `{"enabled": …}` does. With no profile installed, the button is disabled.
2. Each endpoint row, and a `default` row when the profile has one, shows its `in` and `out` delay in milliseconds. Type a new value and press Enter, and the card posts the whole profile back.
3. An edit scales that delay's distribution: typing `200` here makes `p90` `500` and `p99` `1200`, and a `base_ms` edit scales `jitter_ms` with it. A delay at `0` is set flat to the new value.
4. **edited · revert** appears once the profile differs from the one the card read when the page loaded, and posts that one back.
5. An edit takes effect only while shaping is on. The card edits delays, not `reject`.

Done with the card: `rystic stop netui`.

## At boot

`rystic run <product> --network <value>`, or `RYSTIC_NETWORK=<value>` for a container or the binary:

| Value | Effect |
|---|---|
| `off` | shaping off; a built-in profile stays installed, inactive |
| `on` | turns on the built-in profile (Kalshi); does nothing on a simulator without one |
| a file path | loads the file. A bare profile, one with an `endpoints` key and neither `profile` nor `enabled`, is installed and enabled. Any other file is read as the `POST` body above: `{"profile":{…}}` without `"enabled":true` installs with shaping off |

A profile with a `default` but no `endpoints` key refuses the boot with `network body must set enabled and/or profile`. Add `"endpoints":{}`, or wrap it as `{"enabled":true,"profile":{…}}`.

On a plan without the `faults` feature, any value refuses the boot: [Boot configuration](https://www.rystic.ai/docs/boot-configuration.md#boot-inputs).

## Other faults

The `faults` feature also revokes credentials mid-run (`/_rystic/revoke`), duplicates webhook deliveries (`/_rystic/duplicate-delivery`) and caps page sizes (`/_rystic/page-cap`). [Control plane → Routes by plan](https://www.rystic.ai/docs/control-plane.md#routes-by-plan) has each one's body.

---

Source: https://www.rystic.ai/docs/network-profiles · Markdown: https://www.rystic.ai/docs/network-profiles.md
