> For the complete documentation index, see [llms.txt](https://docs.kangal.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kangal.dev/gateway/traffic-management.md).

# Traffic management

Kangal manages traffic with service settings, upstream target selection, and ordered route plugins. Use these controls together: service timeouts bound work, rate limits protect capacity, and circuit breaking prevents a failing backend from consuming every worker.

Test policies on a non-production route before applying them broadly.

## Where traffic controls run

Route plugins execute on the data plane after a route is selected. A plugin entry has these common fields:

| Field              | Default        | Meaning                                                                              |
| ------------------ | -------------- | ------------------------------------------------------------------------------------ |
| `name`             | Required       | Registered plugin identifier such as `rate_limiter`.                                 |
| `enabled`          | `true`         | Disabled entries remain in configuration but do not run.                             |
| `config`           | `{}`           | Plugin-specific settings validated against its schema.                               |
| `trigger`          | Plugin default | A shorthand stage such as `before_request`, `after_response`, `on_error`, or `both`. |
| `stage` / `stages` | Plugin default | Explicit single or multiple execution stages.                                        |
| `priority`         | Plugin default | Higher values execute first within the same stage.                                   |

Kangal rejects unknown plugin names, fields outside strict configuration schemas, invalid stage names, and values outside declared limits. A route update that includes `plugins` replaces the complete plugin array; include every plugin that must remain attached.

## Set timeouts and retries first

Services expose `connect_timeout_ms`, `read_timeout_ms`, and `write_timeout_ms` in milliseconds. The Console accepts `100` through `300000` milliseconds. Keep the connect timeout short enough to move past an unreachable target, and size the read timeout for the backend's legitimate response time.

`retries` controls attempts after retryable failures. Use zero through five; higher values are capped at five by the proxy runtime. By default, only `502`, `503`, and `504` responses are retryable, and retries are limited to replay-safe `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` requests. For requests that can carry untrusted bodies, require an explicit request-size policy at the public edge and route before enabling retries.

Retries multiply backend work. Set an explicit service timeout before increasing the retry count, and never rely on retries for non-idempotent writes.

## Add a request rate limit

The `rate_limiter` plugin can define one window, named time windows, or multiple custom windows. This example permits 120 requests per minute for each source IP:

```json
{
  "name": "rate_limiter",
  "enabled": true,
  "trigger": "before_request",
  "priority": 100,
  "config": {
    "limit": 120,
    "window_seconds": 60,
    "limit_by": "ip",
    "fault_tolerant": false,
    "hide_client_headers": false,
    "error_code": 429,
    "error_message": "Request quota exceeded"
  }
}
```

At least one limit form is required:

* `limit` together with `window_seconds`
* one or more of `second`, `minute`, `hour`, `day`, `month`, or `year`
* a non-empty `limits` array whose items contain `limit` and `window_seconds` or `window_size`

For example, enforce both a burst and hourly allowance:

```json
{
  "name": "rate_limiter",
  "config": {
    "limits": [
      {"name": "burst", "limit": 20, "window_seconds": 10},
      {"name": "hourly", "limit": 1000, "window_seconds": 3600}
    ],
    "limit_by": "consumer"
  }
}
```

### Rate-limit identity

`limit_by` accepts:

| Value        | Bucket identity                                                                                                                             |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `ip`         | First address in `X-Forwarded-For`, otherwise the connected client address. Trust this only when a controlled proxy overwrites that header. |
| `consumer`   | Authenticated consumer state or `X-Consumer-ID`; unauthenticated requests share `anonymous`.                                                |
| `credential` | Credential state or credential/API-key headers.                                                                                             |
| `header`     | Value of `header_name`; missing values share `anonymous`.                                                                                   |
| `path`       | Request path, or the configured `path` in the combined runtime.                                                                             |
| `tenant`     | Current tenant identity.                                                                                                                    |
| `service`    | Selected service identity.                                                                                                                  |
| `route`      | Selected route identity.                                                                                                                    |
| `global`     | One bucket for the configured scope.                                                                                                        |

Use `consumer` or `credential` only after an authentication plugin has populated the corresponding identity. Otherwise unrelated callers can share one bucket.

### Rate-limit responses

For accepted requests, rate-limit metadata is sent upstream and is not a client response contract. Rejected requests include `Retry-After`. `error_code` must be from `400` to `599`; use `429` for rate-limit rejection.

### Counter scope differs by deployment

In the combined application, `rate_limiter` uses Redis and fixed time buckets, so replicas that share the same Redis instance share counters.

In an independently deployed data plane, the same plugin uses process-local buckets. Restarting a process clears its counters. Size each node's limit for the maximum number of active processes, or enforce an exact fleet-wide quota at a shared edge.

`fault_tolerant: true` allows requests when the combined runtime cannot reach Redis. Set it to `false` only when rejecting traffic is safer than temporarily losing quota enforcement.

## Protect backend concurrency and failures

`service_protection` combines an in-flight request cap with a rolling circuit-breaker:

```json
{
  "name": "service_protection",
  "enabled": true,
  "stages": ["before_request", "after_response", "on_error"],
  "priority": 90,
  "config": {
    "namespace": "checkout",
    "max_concurrent_requests": 80,
    "failure_window_seconds": 60,
    "failure_threshold": 5,
    "minimum_requests": 10,
    "failure_rate_threshold": 0.5,
    "cooldown_seconds": 30,
    "failure_status_codes": [500, 502, 503, 504],
    "open_circuit_status": 503,
    "open_circuit_body": "Checkout temporarily unavailable",
    "include_headers": true
  }
}
```

The before stage reserves concurrency and rejects when the circuit is open. The response and error stages release that reservation and record the outcome. Configure all three stages; a before-only entry can leave in-flight accounting incorrect.

The circuit opens only after both `minimum_requests` and `failure_threshold` are met and the failure ratio reaches `failure_rate_threshold`. During cooldown, rejections can include `X-Service-Protection` and `Retry-After`. Responses can include state, failure, and request-count headers when `include_headers` is true.

Concurrency and circuit state are process-local. Each worker and data-plane node protects itself independently, and a restart clears the state. Divide a strict backend-wide concurrency budget across the maximum number of active workers or enforce that budget at the backend.

## Limit request body size

`request_size_limiter` protects `POST`, `PUT`, and `PATCH` bodies:

```json
{
  "name": "request_size_limiter",
  "priority": 200,
  "config": {
    "max_bytes": 1048576
  }
}
```

`max_bytes` is required and is measured in bytes. The plugin applies to buffered `POST`, `PUT`, and `PATCH` bodies. Configure an ingress or application upload limit as well so streaming requests and absolute edge capacity are covered.

## Terminate selected requests

`request_termination` returns a configured response when every specified header has the exact configured value:

```json
{
  "name": "request_termination",
  "priority": 300,
  "config": {
    "header_equals": {
      "X-Maintenance-Block": "true"
    },
    "status_code": 503,
    "message": "Temporarily unavailable"
  }
}
```

Use a trusted edge to set or remove control headers. Do not let public clients decide whether a maintenance or policy rule applies.

## Canary routing

`canary_shadow_traffic` makes deterministic percentage and header decisions for the primary request. A canary target must be a complete HTTP or HTTPS URL:

```json
{
  "name": "canary_shadow_traffic",
  "priority": 150,
  "config": {
    "canary_percentage": 10,
    "bucket_by": "header",
    "bucket_header_name": "X-Customer-ID",
    "bucket_salt": "checkout-v2",
    "canary_header_name": "X-Canary",
    "canary_header_value": "true",
    "canary_upstream_url": "https://checkout-v2.internal.example",
    "include_decision_headers": true
  }
}
```

The stable bucket key keeps the same identity in the same cohort while the salt and percentage remain unchanged. A matching canary decision reroutes the primary request to `canary_upstream_url` or `canary_forward_url`.

## Count completed responses

`response_rate_limiter` checks quota before the request and records matching responses afterward. It defaults to counting `2xx` responses and requires both request and response stages:

```json
{
  "name": "response_rate_limiter",
  "trigger": "both",
  "config": {
    "limit": 100,
    "window_seconds": 60,
    "limit_by": "credential",
    "count_statuses": ["2xx", 429]
  }
}
```

Its counters are process-local. Use it for per-worker shaping and response-cost workflows, not as an exact shared billing counter.

## Apply a complete route policy

This patch attaches a shared request quota and service protection to an existing route. Replace the environment values first:

```bash
export KANGAL_ADMIN_URL="https://control.example.com"
export KANGAL_TOKEN="replace-with-admin-token"
export KANGAL_GATEWAY_ID="gateway_123"
export KANGAL_ROUTE_ID="route_123"

curl -sS -X PATCH \
  "$KANGAL_ADMIN_URL/api/v1/admin/gateways/$KANGAL_GATEWAY_ID/routes/$KANGAL_ROUTE_ID" \
  -H "Authorization: Bearer $KANGAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "plugins": [
      {
        "name": "rate_limiter",
        "enabled": true,
        "trigger": "before_request",
        "priority": 100,
        "config": {
          "minute": 120,
          "limit_by": "ip",
          "fault_tolerant": false
        }
      },
      {
        "name": "service_protection",
        "enabled": true,
        "stages": ["before_request", "after_response", "on_error"],
        "priority": 90,
        "config": {
          "max_concurrent_requests": 80,
          "failure_threshold": 5,
          "minimum_requests": 10,
          "failure_rate_threshold": 0.5,
          "cooldown_seconds": 30
        }
      }
    ]
  }'
```

The equivalent CLI shape is:

```bash
kangalctl update routes "$KANGAL_ROUTE_ID" \
  --gateway-id "$KANGAL_GATEWAY_ID" \
  -f route-policy.json
```

After applying a policy, confirm that the desired configuration version has been acknowledged by the serving data plane before running load tests.

## Validation checklist

1. Send requests below the threshold and inspect quota headers.
2. Exceed the threshold and verify the response code and `Retry-After`.
3. Test independent identities to confirm they receive separate buckets.
4. Generate controlled `5xx` responses and observe the circuit transition.
5. Confirm concurrency returns to zero after successes and errors.
6. Repeat tests against every data-plane node; local policies can differ by process state even with identical configuration.

## Troubleshooting

### Every caller shares one quota

The selected identity is missing. Check authentication order and identity headers, or use `ip` temporarily. For `header`, confirm `header_name` is present on every request.

### A distributed limit is higher than configured

The independent runtime keeps counters per process. Multiply the configured limit by the number of active processes to estimate the fleet-wide maximum, or move the shared limit to the combined Redis-backed runtime or an external edge.

### Redis fails but requests continue

`fault_tolerant` defaults to true in the combined runtime. Set it to false if strict fail-closed quota enforcement is required, and monitor Redis as a traffic dependency.

### The circuit never opens

Confirm the plugin runs in request, response, and error stages. Then check that failure codes, `minimum_requests`, `failure_threshold`, and `failure_rate_threshold` can all be satisfied in the configured window.

### The circuit stays busy after requests finish

The response or error stage is missing, or a request did not pass through the matching plugin lifecycle. Apply all three `service_protection` stages and retest after the process-local state resets.

### A route update removed another plugin

`plugins` is a replacement array. Read the route, merge the desired entries in your configuration file, and patch the complete list.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kangal.dev/gateway/traffic-management.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
