Configure tool-based rate limiting

Last published : Nov 02, 2026
NetScaler® rate limiting enforces request quotas on MCP traffic at the Gateway and on individual MCP routes, giving you control over how much each MCP server, tool, or client can consume. Requests that exceed a configured quota are rejected before they reach the upstream MCP server.
You define a quota with the ratelimit CRD and attach it either to the Gateway (all MCP routes) or to a single mcproute (one MCP server). Because the Gateway understands the MCP protocol, the counter a request is charged against can be keyed on the routing header (per MCP server), the JSON-RPC tool name in the request body (per tool), or any request header such as a caller-identity header (per user) — not just the client IP address.
Requests exceeding the configured quota receive an HTTP/1.1 429 Too Many Requests response before the request reaches the MCP server.
Note:
Rate limiting for MCP is supported on both NetScaler CPX (in-cluster) and NetScaler VPX/MPX/BLX (external) on NetScaler 14.1. The ratelimit CRD is group citrix.com/v1beta1, while mcproute and mcpprofile are group citrix.com/v1.

Per-gateway vs per-route rate limits

A rate limit can be applied globally at the Gateway level (across every MCP route) or to a specific MCP server through an mcproute.
Type How it binds Scope
Per gateway targetRef pointing to the Gateway resource All MCP routes on the Gateway
Per route filters[].extensionRef in an mcproute rule One MCP server route only
The selector_keys field decides which counter a request is charged against:
  • Per MCP server — an advanced selector on the routing header x-netscaler-target-mcp-server. Each backend gets its own budget, so a noisy client of one MCP server cannot exhaust another's quota.
  • Per tool — an advanced selector on the JSON-RPC params.name in the request body. An expensive tool can be capped without affecting the rest of the server.
  • Per user — a basic selector on a request header (for example, a caller-identity header). Each distinct header value gets its own budget.
Note:
Both a per-gateway and a per-route limit can be active at the same time. Requests that exceed either quota receive a 429 Too Many Requests response.

Configuration

Apply a per-MCP-server rate limit at the Gateway

This ratelimit uses targetRef to reference the Gateway directly, so the quota applies to every MCP route on the Gateway. The advanced selector keys the counter on the MCP server name taken from the routing header, giving each backend an independent budget.
The following example allows 30 requests per MCP server over a 60-second window (timeslice is in milliseconds). Requests over the quota are answered with a custom 429 Too Many Requests response that carries a Retry-After header (throttle_action: RESPOND with respond_with).
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1beta1
kind: ratelimit
metadata:
  name: mcp-per-server-ratelimit
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  targetRef:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: mcp-gw
      namespace: default
  selector_keys:
    advanced:
      # Bucket per MCP server: the part of the routing header before the trailing slash.
      rules:
        - 'HTTP.REQ.HEADER("x-netscaler-target-mcp-server").BEFORE_STR("/")'
      include_base_selectors: false
  goto_priority_expression: END
  ratelimits:
    - req_threshold: 30
      timeslice: 60000
      limittype: BURSTY
      mode: REQUEST_RATE
      throttle_action: "RESPOND"
      respond_with: '"HTTP/1.1 429 Too Many Requests\r\nContent-Type: application/json\r\nRetry-After: 60\r\nConnection: close\r\n\r\n{\"error\":\"Rate limit exceeded for MCP backend - retry after 60s\"}"'
EOF
The selector keys on the routing header the client sends, for example x-netscaler-target-mcp-server: everything.mcp.example/. The trailing slash is required: the expression takes the value BEFORE_STR("/"), so everything.mcp.example/ is charged to the everything.mcp.example counter. Routing on this header is covered in MCP routing.
Important:
goto_priority_expression: END is required whenever throttle_action is RESPOND. A RESPOND action is a terminating responder, and NetScaler rejects a terminating responder bound with NEXT.

Apply a per-tool rate limit on an MCP route

This ratelimit caps a single tool. The advanced selector reads the JSON-RPC params.name from the request body, so two different tool names get two different counters. The match narrows the limit to tools/call, which keeps the initialize and tools/list handshake — that every client must complete — outside the bucket.
The following example allows 3 calls of any one tool over a 120-second window. Requests over the quota are answered with a JSON-RPC error (code -32003) that echoes the caller's jsonrpc and id, so an MCP client renders it as a tool error in the conversation instead of a transport failure.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1beta1
kind: ratelimit
metadata:
  name: mcp-everything-tool-ratelimit
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  selector_keys:
    advanced:
      # Bucket per tool name from the JSON-RPC body.
      rules:
        - 'HTTP.REQ.BODY(100000).XPATH_JSON(xp%/params/name%)'
      include_base_selectors: false
      # Count only tools/call, so the handshake stays outside the quota.
      match: 'http.req.header("Content-Type").EQ("application/json") && http.req.body(100000).xpath_json(xp%boolean(//method)%) && http.req.body(100000).xpath_json(xp%/method%).eq("tools/call")'
  goto_priority_expression: END
  ratelimits:
    - req_threshold: 3
      timeslice: 120000
      limittype: BURSTY
      mode: REQUEST_RATE
      throttle_action: "RESPOND"
      respond_with: '"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"jsonrpc\":\"" + http.req.body(100000).xpath_json(xp%/jsonrpc%) + "\",\"id\":" + http.req.body(100000).xpath_json(xp%/id%) + ",\"error\":{\"code\":-32003,\"message\":\"Rate limit exceeded for this tool. Retry in 2 minutes.\"}}"'
EOF

Bind the rate limit to the MCP route

A per-route ratelimit takes effect only when an mcproute rule references it through an ExtensionRef filter. For extensionRef, the group is always citrix.com and the kind is ratelimit. The following mcproute routes the everything MCP server and applies the per-tool quota created above. The backend profile is referenced by a second ExtensionRef filter.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-everything
  namespace: default
spec:
  parentRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - name: x-netscaler-target-mcp-server
              type: MCPTargetServer
              value: everything.mcp.example
      filters:
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: mcpprofile
            name: mcp-everything-backend
        - type: ExtensionRef
          extensionRef:
            group: citrix.com
            kind: ratelimit
            name: mcp-everything-tool-ratelimit
      backendRefs:
        - kind: Service
          name: mcp-everything
          port: 3001
EOF
Note:
The header match value in the mcproute has no trailing slash (everything.mcp.example), while the client sends the header with the trailing slash (everything.mcp.example/).

Rate limit per user

To cap each caller individually, use a basic selector keyed on a request header that identifies the user (for example, a header set by your client or an upstream authentication layer). Each distinct header value gets its own counter. This limit can be attached to the Gateway with targetRef (as shown) or to a single MCP route with an ExtensionRef filter.
The following example allows each user 30 requests over a 60-second window. With throttle_action: RESPOND and no respond_with, exceeding the quota returns a default 429 Too Many Requests response.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1beta1
kind: ratelimit
metadata:
  name: mcp-per-user-ratelimit
  namespace: default
spec:
  gatewayClassName: "mcpgwclass"
  targetRef:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: mcp-gw
      namespace: default
  selector_keys:
    basic:
      header_name: "X-User-Id"
  goto_priority_expression: END
  ratelimits:
    - req_threshold: 30
      timeslice: 60000
      limittype: BURSTY
      mode: REQUEST_RATE
      throttle_action: "RESPOND"
EOF

Reference