spring-projects/spring-ai · error · ToolCallLimitExceededException
Tool call limit exceeded (dynamic message: tool name and lim
Error message
Tool call limit exceeded (dynamic message: tool name and limit)
What it means
Spring AI's DefaultToolCallingManager enforces a per-tool call limit (ToolCallingChatOptions maxCalls). When ToolCallLimitBehavior.THROW_EXCEPTION is configured and a tool exceeds its allowed number of invocations, the manager throws ToolCallLimitExceededException carrying the tool name, the limit, and a partial ToolExecutionResult built from the conversation history up to that point. It exists to stop runaway LLM tool-calling loops.
Source
Thrown at spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java:262
totalToolCallCount++;
int toolCallCount = toolCallCounts.merge(toolName, 1, Integer::sum);
ToolCallLimits.Breach limitBreach = this.toolCallLimits.check(toolName, toolCallCount, totalToolCallCount);
if (limitBreach != null) {
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, limitBreach.message()));
if (this.toolCallLimits.onLimitExceeded() == ToolCallLimitBehavior.THROW) {
ToolResponseMessage partialToolResponseMessage = ToolResponseMessage.builder()
.responses(toolResponses)
.build();
List<Message> partialConversationHistory = buildConversationHistoryAfterToolExecution(
prompt.getInstructions(), assistantMessage, partialToolResponseMessage);
ToolExecutionResult partialToolExecutionResult = ToolExecutionResult.builder()
.conversationHistory(partialConversationHistory)
.returnDirect(Objects.requireNonNullElse(returnDirect, false))
.build();
throw new ToolCallLimitExceededException(limitBreach.toolName(), limitBreach.limit(),
partialToolExecutionResult);
}
// ToolCallLimitBehavior.RETURN_ERROR_RESPONSE: skip invoking this tool
// call but keep processing the rest of the batch.
continue;
}
String toolInputArguments = toolCall.arguments();
// Handle the possible null parameter situation in streaming mode.
final String finalToolInputArguments;
if (!StringUtils.hasText(toolInputArguments)) {
if (logger.isWarnEnabled()) {
logger.warn("Tool call arguments are null or empty for tool: " + toolName
+ ". Using empty JSON object as default.");
}
finalToolInputArguments = "{}";View on GitHub (pinned to 98a7beda4f)
Solutions
- Raise the per-tool call limit (e.g. toolCallLimit("myTool", 10)) so legitimate workflows complete
- Change the limit behavior to ToolCallLimitBehavior.RETURN_ERROR_RESPONSE so the model receives an error response instead of an exception
- Catch ToolCallLimitExceededException and use getResult().conversationHistory() from the partial result to continue or summarize
- Improve tool responses/descriptions so the model stops re-invoking the tool
Example fix
// before
ChatOptions options = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.toolCallLimit("search", 2, ToolCallLimitBehavior.THROW_EXCEPTION)
.build();
// after
ChatOptions options = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.toolCallLimit("search", 10, ToolCallLimitBehavior.RETURN_ERROR_RESPONSE)
.build(); Defensive patterns
Strategy: try-catch
Validate before calling
// Before calling the model, ensure limits are sensible
ToolCallingChatOptions opts = ...;
if (opts.getToolCallLimit() != null && opts.getToolCallLimit() < expectedMaxToolCalls) {
throw new IllegalArgumentException("tool call limit too low for this workflow");
} Try / catch
try {
ChatResponse resp = chatModel.call(prompt);
} catch (ToolCallLimitExceededException e) {
// inspect e.getToolName(), e.getLimit(); continue from e.getResult().conversationHistory()
logger.warn("Tool {} hit call limit {}", e.getToolName(), e.getLimit());
} Prevention
- Set per-tool limits generously (workflow max steps + margin)
- Prefer ToolCallLimitBehavior.RETURN_ERROR_RESPONSE in production agents
- Monitor how many tool calls each conversation actually consumes and tune limits
When it happens
Trigger: Calling ChatClient/ChatModel with ToolCallingChatOptions where a tool limit is registered via toolCallLimit(toolName, maxCalls) with behavior THROW_EXCEPTION, and the model then requests that tool more than maxCalls times during internalToolExecutionResult processing; executeToolCall detects the breach via limitBreach and throws.
Common situations: LLMs entering tool-calling loops (repeatedly calling a search or fetch tool because results don't satisfy them), setting maxCalls too low for legitimate multi-step workflows, or agents with recursive/self-referencing tools.
Related errors
- No ToolCallback found for tool name: %s
- Multiple tools with the same name (%s) found in ToolCallingC
- HTTP %s - %s
- Async complete methods should use McpAsyncRequestContext ins
- Method cannot have more than one McpMeta parameter: ${method
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/d4fc5e2198b62238.
Report an issue: GitHub.