KubeMQ
AiwayAI Agents (A2A)Guides

Concurrency & Limits

Per-agent concurrency caps, the 10MB response-size limit, and timeout capping that protect the A2A gateway from overload and runaway agents.

The A2A gateway protects itself and your agents with three hard limits: a per-agent concurrency cap, a response-size cap, and timeout capping. Each is enforced by the agent's virtual subscriber, returns a predictable error when crossed, and is tunable through A2aConfig. This guide explains the behavior of each limit and shows how to observe it from a client.

Overview

Every registered agent runs behind a virtual subscriber — the bridge that turns an inbound JSON-RPC request into an outbound HTTP POST to the agent's URL. That subscriber is where the limits live, so they apply per agent, not per client. Multiple callers hitting the same agent_id share the same budget.

LimitConfig fieldDefaultWhat happens when exceeded
Concurrent in-flight requestsAgentMaxConcurrency100Overflow request rejected immediately — Executed: false, JSON-RPC -32603
Agent response body sizeAgentMaxResponseBytes10485760 (10 MB)Response dropped — Executed: false, JSON-RPC -32603
Per-request timeoutDefaultTimeoutSeconds / MaxTimeoutSeconds300 / 3600Capped silently, or -32001 (agent timeout) on expiry

All three surface as transport errors (Executed: false) — the request never reached your agent's business logic, so a retry is generally safe. See Error handling for the transport-vs-application distinction.

Per-agent concurrency limit

Each agent's virtual subscriber holds a buffered-channel semaphore sized to AgentMaxConcurrency (default 100). The count covers both synchronous (message/send) and streaming (message/stream) requests in flight at the same time.

Request #Behavior
1–100A handler is spawned and the request is proxied to the agent
101+Immediately rejected — no goroutine spawned, the agent is never called

When the semaphore is full, the overflow request comes back as a JSON-RPC -32603 error:

{
  "jsonrpc": "2.0",
  "id": 101,
  "error": {
    "code": -32603,
    "message": "internal error: concurrency limit exceeded"
  }
}

At the transport layer this is recorded as Executed: false with the reason server busy: concurrency limit reached. Because the request was never processed, retrying after a short backoff is safe.

The limit is per agent, not per client. To raise headroom for a busy agent, increase AgentMaxConcurrency in configuration — or run more agent instances behind distinct agent_ids and route across them.

Observe the limit

Fire more than AgentMaxConcurrency requests at one agent simultaneously and at least one comes back with -32603. The snippets below send 101 concurrent message/send requests and count how many were rejected.

# Fire 101 requests in parallel; at least one returns -32603 "concurrency limit exceeded".
for i in $(seq 1 101); do
  curl -s -X POST http://localhost:9090/a2a/concurrency-agent-01 \
    -H "Content-Type: application/json" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":$i,\"method\":\"message/send\",\"params\":{\"message\":{\"parts\":[{\"text\":\"Request #$i\"}]}}}" &
done | grep -c -- -32603
wait
# => prints the number of requests rejected by the concurrency limit (>= 1)
using System.Text;
using System.Text.Json.Nodes;

const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "concurrency-agent-01";
const int NumRequests = 101;

async Task<JsonNode?> SendRequest(HttpClient httpClient, int requestId)
{
    var payload = new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = requestId,
        ["method"] = "message/send",
        ["params"] = new JsonObject
        {
            ["message"] = new JsonObject
            {
                ["parts"] = new JsonArray(new JsonObject { ["text"] = $"Request #{requestId}" })
            }
        }
    };
    var resp = await httpClient.PostAsync(
        $"{KubeMqUrl}/a2a/{AgentId}",
        new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
    return JsonNode.Parse(await resp.Content.ReadAsStringAsync());
}

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
Console.WriteLine($"Sending {NumRequests} concurrent requests (limit is 100)...");

var tasks = Enumerable.Range(1, NumRequests)
    .Select(i => SendRequest(client, i))
    .ToArray();

var results = await Task.WhenAll(tasks);

int successes = 0, concurrencyErrors = 0, otherErrors = 0;
foreach (var r in results)
{
    if (r?["result"] != null)
        successes++;
    else if (r?["error"]?["code"]?.GetValue<int>() == -32603)
        concurrencyErrors++;
    else
        otherErrors++;
}

Console.WriteLine($"  Successes:          {successes}");
Console.WriteLine($"  Concurrency errors: {concurrencyErrors} (code -32603)");
Console.WriteLine($"  Other errors:       {otherErrors}");

if (concurrencyErrors >= 1)
    Console.WriteLine($"\nConcurrency limit enforced — {concurrencyErrors} request(s) rejected!");
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"
)

const (
	kubemqURL   = "http://localhost:9090"
	agentID     = "concurrency-agent-01"
	numRequests = 101
)

type result struct {
	data map[string]interface{}
	err  error
}

func sendRequest(id int, client *http.Client, wg *sync.WaitGroup, results chan<- result) {
	defer wg.Done()
	payload := map[string]interface{}{
		"jsonrpc": "2.0",
		"id":      id,
		"method":  "message/send",
		"params": map[string]interface{}{
			"message": map[string]interface{}{
				"parts": []map[string]interface{}{{"text": fmt.Sprintf("Request #%d", id)}},
			},
		},
	}
	data, _ := json.Marshal(payload)
	resp, err := client.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(data))
	if err != nil {
		results <- result{err: err}
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	var r map[string]interface{}
	json.Unmarshal(body, &r)
	results <- result{data: r}
}

func main() {
	fmt.Printf("Sending %d concurrent requests (limit is 100)...\n", numRequests)

	client := &http.Client{Timeout: 30 * time.Second}
	results := make(chan result, numRequests)
	var wg sync.WaitGroup

	for i := 1; i <= numRequests; i++ {
		wg.Add(1)
		go sendRequest(i, client, &wg, results)
	}
	wg.Wait()
	close(results)

	successes, concurrencyErrors, otherErrors := 0, 0, 0
	for r := range results {
		if r.err != nil {
			otherErrors++
		} else if _, ok := r.data["result"]; ok {
			successes++
		} else if e, ok := r.data["error"].(map[string]interface{}); ok {
			if code, _ := e["code"].(float64); int(code) == -32603 {
				concurrencyErrors++
			} else {
				otherErrors++
			}
		}
	}

	fmt.Printf("  Successes:          %d\n", successes)
	fmt.Printf("  Concurrency errors: %d (code -32603)\n", concurrencyErrors)
	fmt.Printf("  Other errors:       %d\n", otherErrors)

	if concurrencyErrors >= 1 {
		fmt.Printf("\nConcurrency limit enforced — %d request(s) rejected!\n", concurrencyErrors)
	}
}
import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class Client {

    static final String KUBEMQ_URL = "http://localhost:9090";
    static final String AGENT_ID = "concurrency-agent-01";
    static final int NUM_REQUESTS = 101;
    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        var client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();

        System.out.println("Sending " + NUM_REQUESTS + " concurrent requests (limit is 100)...");

        List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
        for (int i = 1; i <= NUM_REQUESTS; i++) {
            var payload = Map.of(
                "jsonrpc", "2.0",
                "id", i,
                "method", "message/send",
                "params", Map.of(
                    "message", Map.of("parts", List.of(Map.of("text", "Request #" + i)))
                )
            );
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/a2a/" + AGENT_ID))
                .header("Content-Type", "application/json")
                .timeout(Duration.ofSeconds(30))
                .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
                .build();
            futures.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()));
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

        int successes = 0, concurrencyErrors = 0, otherErrors = 0;
        for (var future : futures) {
            var data = MAPPER.readTree(future.get().body());
            if (data.has("result")) {
                successes++;
            } else if (data.has("error")) {
                if (data.path("error").path("code").asInt() == -32603) concurrencyErrors++;
                else otherErrors++;
            }
        }

        System.out.println("  Successes:          " + successes);
        System.out.println("  Concurrency errors: " + concurrencyErrors + " (code -32603)");
        System.out.println("  Other errors:       " + otherErrors);

        if (concurrencyErrors >= 1)
            System.out.println("\nConcurrency limit enforced — " + concurrencyErrors + " request(s) rejected!");
    }
}
import asyncio

import httpx

KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "concurrency-agent-01"
NUM_REQUESTS = 101


async def send_request(client: httpx.AsyncClient, request_id: int) -> dict:
    payload = {
        "jsonrpc": "2.0",
        "id": request_id,
        "method": "message/send",
        "params": {
            "message": {"parts": [{"text": f"Request #{request_id}"}]},
        },
    }
    resp = await client.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload)
    return resp.json()


async def main() -> None:
    limits = httpx.Limits(max_connections=200)
    async with httpx.AsyncClient(timeout=30, limits=limits) as client:
        print(f"Sending {NUM_REQUESTS} concurrent requests (limit is 100)...")
        tasks = [send_request(client, i) for i in range(1, NUM_REQUESTS + 1)]
        results = await asyncio.gather(*tasks)

    successes = sum(1 for r in results if "result" in r)
    concurrency_errors = sum(1 for r in results if r.get("error", {}).get("code") == -32603)
    other_errors = len(results) - successes - concurrency_errors

    print(f"  Successes:          {successes}")
    print(f"  Concurrency errors: {concurrency_errors} (code -32603)")
    print(f"  Other errors:       {other_errors}")

    assert concurrency_errors >= 1, "Expected at least 1 concurrency limit error"
    print(f"\nConcurrency limit enforced — {concurrency_errors} request(s) rejected!")


if __name__ == "__main__":
    asyncio.run(main())
const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "concurrency-agent-01";
const NUM_REQUESTS = 101;

async function sendRequest(id: number): Promise<{ ok: boolean; errorCode?: number }> {
  const request = {
    jsonrpc: "2.0",
    id,
    method: "message/send",
    params: { message: { parts: [{ text: `Request ${id}` }] } },
  };

  const resp = await fetch(`${KUBEMQ_URL}/a2a/${AGENT_ID}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(request),
  });
  const data = await resp.json();
  if (data.error) {
    return { ok: false, errorCode: data.error.code };
  }
  return { ok: true };
}

async function main() {
  console.log(`Sending ${NUM_REQUESTS} concurrent requests (limit is 100)...`);

  const promises = Array.from({ length: NUM_REQUESTS }, (_, i) => sendRequest(i + 1));
  const results = await Promise.all(promises);

  const succeeded = results.filter((r) => r.ok).length;
  const rejected = results.filter((r) => r.errorCode === -32603).length;
  const otherErrors = results.length - succeeded - rejected;

  console.log(`  Succeeded:          ${succeeded} (expect <=100)`);
  console.log(`  Rejected -32603:    ${rejected} (expect >=1)`);
  console.log(`  Other errors:       ${otherErrors}`);

  if (rejected > 0) {
    console.log(`\nConcurrency limit enforced correctly.`);
  }
}

main().catch(console.error);

Response-size limit

The gateway caps the agent's HTTP response body at AgentMaxResponseBytes (default 10 MB). When an agent returns more than that — for a synchronous reply, or for a single SSE event's accumulated data lines on a stream — the gateway aborts the relay to protect itself from memory exhaustion and returns -32603:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32603,
    "message": "internal error: response too large"
  }
}

At the transport layer this is Executed: false with the reason agent response too large. Unlike a timeout, retrying the same request will fail identically — the agent is producing an oversized body. Fix it by paginating the agent's output, streaming the result via message/stream, or raising AgentMaxResponseBytes if the payload is legitimately large.

Observe the limit

Target an agent that returns more than 10 MB and inspect the error object.

curl -s -X POST http://localhost:9090/a2a/oversize-agent-01 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {"message": {"parts": [{"text": "Give me a large response"}]}}
  }'
# => {"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"internal error: response too large"}}
using System.Text;
using System.Text.Json.Nodes;

const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "oversize-agent-01";

var payload = new JsonObject
{
    ["jsonrpc"] = "2.0",
    ["id"] = 1,
    ["method"] = "message/send",
    ["params"] = new JsonObject
    {
        ["message"] = new JsonObject
        {
            ["parts"] = new JsonArray(new JsonObject { ["text"] = "Give me a large response" })
        }
    }
};

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
Console.WriteLine("Requesting oversized response (>10MB)...");

var resp = await client.PostAsync(
    $"{KubeMqUrl}/a2a/{AgentId}",
    new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));

var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;

if (data["error"] != null)
{
    var error = data["error"]!;
    Console.WriteLine($"Error code:    {error["code"]}");
    Console.WriteLine($"Error message: {error["message"]}");
    Console.WriteLine("\nResponse size limit enforced!");
}
else
{
    Console.WriteLine($"Status: {(int)resp.StatusCode}");
    Console.WriteLine("Note: Response was accepted (check AgentMaxResponseBytes)");
}
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

const (
	kubemqURL = "http://localhost:9090"
	agentID   = "oversize-agent-01"
)

func main() {
	payload := map[string]interface{}{
		"jsonrpc": "2.0",
		"id":      1,
		"method":  "message/send",
		"params": map[string]interface{}{
			"message": map[string]interface{}{
				"parts": []map[string]interface{}{{"text": "Give me a large response"}},
			},
		},
	}

	data, _ := json.Marshal(payload)
	fmt.Println("Requesting oversized response (>10MB)...")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(data))
	if err != nil {
		fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
		os.Exit(1)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result map[string]interface{}
	json.Unmarshal(body, &result)

	if errorObj, ok := result["error"].(map[string]interface{}); ok {
		code, _ := errorObj["code"].(float64)
		msg, _ := errorObj["message"].(string)
		fmt.Printf("Error code:    %.0f\n", code)
		fmt.Printf("Error message: %s\n", msg)
		fmt.Println("\nResponse size limit enforced!")
	} else {
		fmt.Printf("Status: %d\n", resp.StatusCode)
		fmt.Println("Note: Response was accepted (check AgentMaxResponseBytes)")
	}
}
import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Map;

public class Client {

    static final String KUBEMQ_URL = "http://localhost:9090";
    static final String AGENT_ID = "oversize-agent-01";
    static final ObjectMapper MAPPER = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        var payload = Map.of(
            "jsonrpc", "2.0",
            "id", 1,
            "method", "message/send",
            "params", Map.of(
                "message", Map.of("parts", List.of(Map.of("text", "Give me a large response")))
            )
        );

        var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
        var req = HttpRequest.newBuilder()
            .uri(URI.create(KUBEMQ_URL + "/a2a/" + AGENT_ID))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
            .build();

        System.out.println("Requesting oversized response (>10MB)...");
        var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
        var data = MAPPER.readTree(resp.body());

        if (data.has("error")) {
            var error = data.path("error");
            System.out.println("Error code:    " + error.path("code").asInt());
            System.out.println("Error message: " + error.path("message").asText());
            System.out.println("\nResponse size limit enforced!");
        } else {
            System.out.println("Status: " + resp.statusCode());
            System.out.println("Note: Response was accepted (check AgentMaxResponseBytes)");
        }
    }
}
import asyncio

import httpx

KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "oversize-agent-01"


async def main() -> None:
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "message/send",
        "params": {
            "message": {"parts": [{"text": "Give me a large response"}]},
        },
    }

    async with httpx.AsyncClient(timeout=30) as client:
        print("Requesting oversized response (>10MB)...")
        resp = await client.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload)
        data = resp.json()

        if "error" in data:
            error = data["error"]
            print(f"Error code:    {error.get('code')}")
            print(f"Error message: {error.get('message')}")
            print("\nResponse size limit enforced!")
        else:
            print(f"Status: {resp.status_code}")
            print("Note: Response was accepted (check AgentMaxResponseBytes)")


if __name__ == "__main__":
    asyncio.run(main())
const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "oversize-agent-01";

async function main() {
  const request = {
    jsonrpc: "2.0",
    id: 1,
    method: "message/send",
    params: {
      message: { parts: [{ text: "Give me a large response" }] },
    },
  };

  console.log("Requesting oversized response (>10MB)...");

  const resp = await fetch(`${KUBEMQ_URL}/a2a/${AGENT_ID}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(request),
  });

  const data = await resp.json();

  if (data.error) {
    console.log(`Error code:    ${data.error.code}`);
    console.log(`Error message: ${data.error.message}`);
    console.log("\nResponse size limit enforced!");
  } else {
    console.log("Unexpected: got success response. Response may have been under the limit.");
  }
}

main().catch(console.error);

Timeout capping

Each call carries a deadline. When a client sets params.configuration.timeout (in seconds), the gateway clamps it to the configured window before proxying to the agent:

  • If no timeout is supplied, the server applies DefaultTimeoutSeconds (300s).
  • Any value above MaxTimeoutSeconds (3600s) is silently reduced to that ceiling.
  • When the deadline passes before the agent replies, the call returns -32001 (agent timeout).
params.configuration.timeoutEffective deadline
not setDefaultTimeoutSeconds (300s)
3030s
36003600s
999993600s (capped to MaxTimeoutSeconds)

A gateway buffer of ~10 seconds (GatewayTimeoutBuffer) is added on top of the effective deadline before the gateway times out its proxy request, so the gateway never gives up before the agent's own deadline. Give your HTTP client a slightly longer timeout than the value you request (a 15s pad is typical) so the client does not abort before the gateway returns the -32001 envelope.

Monitoring

Concurrency and limit pressure are visible in the Prometheus metrics exported on port 8080 — see Observability.

MetricWhat it tells you
kubemq_a2a_requests_totalTotal requests per agent and method — rising rejections track concurrency pressure
kubemq_a2a_errors_totalError count per agent — -32603 spikes signal a saturated agent
kubemq_a2a_sse_streams_activeActive SSE streams per agent (each counts against the concurrency budget)

Was this page helpful?

On this page