# Kill Switches

- Canonical URL: https://docs.fairvisor.com/docs/policy/kill-switches/
- Section: docs
- Last updated: n/a
> Instant traffic blocking matched on any request descriptor, with optional TTL.


Kill switches provide an emergency brake. They are evaluated **before** any route matching or policy rules, making them the fastest and most authoritative way to block traffic.

If top-level `kill_switch_override` is active, this pre-check is skipped for its TTL window.

## Structure

Kill switches live in the top-level `kill_switches` array:

```json
{
  "bundle_version": 2,
  "kill_switches": [
    {
      "scope_key": "header:x-tenant-id",
      "scope_value": "tenant-compromised",
      "reason": "incident-2026-01-15",
      "expires_at": "2026-01-16T00:00:00Z"
    }
  ],
  "policies": [ ... ]
}
```

## Fields

| Field | Type | Required | Description |
|---|---|---|---|
| `scope_key` | string | **yes** | Descriptor key to match against. Format: `source:name`. See [Descriptor Keys](/docs/policy/rules/#descriptor-keys). |
| `scope_value` | string | **yes** | Exact value to match. Case-sensitive. |
| `route` | string | no | If set, only applies when the request path equals this string exactly. |
| `reason` | string | no | Human-readable reason string; included in the `X-Fairvisor-Reason` response header. |
| `expires_at` | string | no | ISO 8601 UTC. After this time the entry is ignored. Evaluated on every request — see note below. |

`scope_key` must match the pattern `^(jwt|header|query|ip|ua):[A-Za-z0-9_-]+$`.

<div class="callout callout-warning">
  <span class="callout-icon">⚠️</span>
  <div><p><strong><code>expires_at</code> is evaluated on every request</strong>, not just at load time. Once the timestamp passes, the kill switch entry stops matching automatically — no bundle reload required. This is different from the top-level bundle <code>expires_at</code>, which is only checked when the bundle is loaded.</p></div>
</div>

## Evaluation

The engine iterates all kill switch entries. For each entry:

1. If `expires_at` is set and the current time is past that value → **skip** the entry
2. Extract the descriptor for `scope_key` from the request context
3. If the descriptor value equals `scope_value`:
   - If `route` is set: also require the request path to equal `route`
   - If both conditions are satisfied → **reject** with `Retry-After: 3600`

**First match wins.** Subsequent entries are not evaluated after a match.

## Response on match

```
HTTP 429 Too Many Requests
X-Fairvisor-Reason: kill_switch
Retry-After: 3600
```

## Common use cases

### Block a compromised tenant

```json
{
  "scope_key": "header:x-tenant-id",
  "scope_value": "tenant-42",
  "reason": "account_suspended"
}
```

### Block a specific IP

```json
{
  "scope_key": "ip:address",
  "scope_value": "203.0.113.5",
  "reason": "abuse"
}
```

### Block all bot traffic on one route

```json
{
  "scope_key": "ua:bot",
  "scope_value": "true",
  "route": "/v1/chat/completions",
  "reason": "scraper_block"
}
```

### Temporary block with auto-expiry

```json
{
  "scope_key": "jwt:org_id",
  "scope_value": "org-abc",
  "reason": "billing_hold",
  "expires_at": "2026-02-01T00:00:00Z"
}
```

After the expiry date passes, the entry is silently skipped without requiring a bundle update.

### Block by ASN

```json
{
  "scope_key": "ip:asn",
  "scope_value": "AS64496",
  "reason": "datacenter_block"
}
```

## Activating a kill switch without a full bundle redeploy

Because kill switches are part of the bundle, the fastest way to activate one is via the SaaS dashboard (which pushes an updated bundle to all connected edges) or by incrementing `bundle_version` and triggering a file reload with `kill -HUP $(pidof nginx)`.

> **Tip:** Keep `bundle_version` increments small (e.g., +1 each edit). Avoid large jumps that could skip valid intermediate versions.

## Temporarily disabling kill switch checks

In break-glass scenarios you can temporarily disable kill-switch enforcement with top-level `kill_switch_override`:

```json
{
  "kill_switch_override": {
    "enabled": true,
    "reason": "incident-2026-02-20",
    "expires_at": "2026-02-20T19:00:00Z"
  }
}
```

Notes:

- This override is global (not per-tenant/per-route)
- It auto-expires at `expires_at`
- No client-visible response header is added; verify activation through logs and metrics

