Rust AI Agent Gateway: A Practical Tokio and Axum Design
A practical Rust AI agent gateway uses Axum for HTTP routing and Tokio for asynchronous I/O, while keeping provider calls, tool execution, and conversation state behind explicit interfaces. The design below shows how to accept a request, enforce limits, call an agent or model service, and return a result without letting slow or untrusted work overwhelm the server.
What the gateway should do
An agent gateway is a service boundary, not necessarily the agent itself. It receives client requests, authenticates and validates them, applies policy, then coordinates model-provider calls and any permitted tools. It can also normalize provider-specific formats and expose a stable API to applications.
Start with a narrow responsibility set. For example, a request might contain a conversation identifier, user message, and optional tool permissions. The gateway checks the caller’s identity and request size, loads authorized state, invokes an agent service, and returns either a completed response or a stream. Avoid coupling HTTP handlers directly to one model vendor or a particular agent framework.
- Axum: routes requests and extracts typed data, shared state, and request metadata.
- Tokio: runs asynchronous tasks, network I/O, timers, and bounded background work.
- Application services: implement orchestration, policy checks, provider selection, and tool access.
- Adapters: translate between the gateway’s internal types and provider or tool APIs.
Separate HTTP, orchestration, and adapters
Keep handlers thin. An Axum handler should parse and validate input, obtain a request-scoped identity, call an application service, and map its result to an HTTP response. It should not contain the full agent loop, provider-specific JSON, retry policy, and database transaction logic all at once.
A useful dependency direction is: HTTP routes call an application service; the application service depends on traits for model and tool operations; concrete adapters implement those traits. This makes provider changes and tests easier, while keeping the agent’s decisions in a place that can be reviewed independently of transport details.
#[derive(Clone)]
struct AppState {
agent: std::sync::Arc<dyn AgentService>,
}
async fn create_run(
axum::extract::State(state): axum::extract::State<AppState>,
axum::Json(input): axum::Json<RunInput>,
) -> Result<axum::Json<RunOutput>, ApiError> {
let output = state.agent.run(input).await?;
Ok(axum::Json(output))
}
The snippet is a shape, not a complete server: production code also needs authentication, input limits, error conversion, tracing, and graceful shutdown. Store shared services in Axum state using cloneable handles such as Arc; do not create a new HTTP client or connection pool for every request.
Tokio concurrency and backpressure
Tokio lets a task wait on network operations without occupying a thread for the entire wait. That does not make every operation cheap or safe to run without limits. Model requests, tool calls, and streams all consume connections, memory, provider capacity, and often money. Put explicit bounds around them.
- Set request deadlines and provider timeouts; propagate cancellation when the client disconnects where practical.
- Use a semaphore or another bounded admission mechanism to cap concurrent expensive runs.
- Use bounded channels for background queues. An unbounded queue can turn overload into steadily growing memory use.
- Keep blocking or CPU-heavy work off Tokio’s async worker threads. Use an appropriate blocking pool or a separate worker service.
- Use retries selectively: retry only operations that are safe to repeat, and apply limits and backoff rather than retrying indefinitely.
For example, a gateway can acquire a permit before starting a provider call and return a clear overload response if capacity is exhausted. The right limit depends on provider quotas, latency, memory, and deployment size; it is not a universal constant. A timeout also needs thought: cancelling the local future does not guarantee that a remote provider has stopped processing its request.
Agent loops, tools, and streaming
An agent loop commonly alternates between a model response and a tool result until it reaches a final answer or a configured stopping condition. Make that loop explicit: set a maximum number of turns, constrain tool names and arguments, validate each tool call, and define what happens when a tool fails. A model’s proposal to call a tool is not authorization to execute it.
Give tools narrow capabilities. A tool that reads a specific record is safer to reason about than one that accepts arbitrary SQL or shell commands. Check authorization on the server for every operation, even if the client or model supplied an earlier permission flag. Treat model output, retrieved documents, and tool results as untrusted input; do not let instructions inside them silently expand permissions.
For streaming, Axum can return a streaming response, commonly using server-sent events or another protocol chosen for the client. Define event types and terminal behavior, such as completion and error events, and ensure the stream is cancelled or cleaned up when the connection closes. Streaming improves perceived responsiveness, but it complicates retries, partial results, and usage accounting. Document whether a stream can be resumed; do not imply resumability unless state and event identifiers support it.
Errors, security, and observability
Use structured errors internally, then map them to stable public responses. Keep provider details, credentials, stack traces, and sensitive tool output out of client-facing error messages. Distinguish invalid input, unauthenticated or unauthorized access, rate or capacity limits, timeouts, and upstream failures so clients can respond sensibly.
Apply authentication and authorization before expensive work. Add request-body limits, rate limits appropriate to the deployment, and validation for identifiers and tool arguments. Store secrets in deployment configuration or a secret-management system, never in source code or logs. Avoid logging full prompts and tool results by default: they may contain personal, confidential, or credential-like data.
Instrument request duration, queue wait, provider latency, tool outcomes, timeouts, and failure categories. Correlate events with a request or run ID, but avoid using user text or other high-cardinality sensitive data as metric labels. Tracing is especially useful for following a run across the HTTP handler, orchestration layer, and adapters.
Testing and deployment choices
Test behavior at multiple boundaries rather than relying only on end-to-end calls to a live model. Unit-test policy and orchestration with fake provider and tool implementations; test HTTP status codes, validation, and serialization through the router; then use a smaller number of integration tests against actual infrastructure where necessary.
- Test that unauthorized tool calls are rejected and that each run respects its turn limit.
- Test provider timeout, malformed response, and tool failure paths.
- Test overload behavior and client disconnect handling, especially for streaming routes.
- Verify graceful shutdown stops accepting new work and allows bounded in-flight work to finish or cancel.
Tokio and Axum are a good fit when the service is primarily asynchronous HTTP and network I/O, and when the team wants explicit control over Rust types and service boundaries. A framework or hosted agent platform may be preferable if it already provides required orchestration, persistence, or operational features. The gateway is not automatically cheaper or more reliable: model usage, tool infrastructure, observability, and on-call operations remain part of the system’s cost.
Frequently Asked Questions
Should the agent loop live inside the Axum handler?
Usually not. Keep the handler focused on HTTP concerns and delegate orchestration to an application service. This makes the loop easier to test, reuse, and change without entangling it with routing.
Does Tokio make model calls run in parallel automatically?
No. Tokio provides asynchronous execution, but your code determines which tasks run concurrently. Use explicit concurrency limits and consider whether parallel tool calls are safe and useful.
How should a Rust gateway handle provider changes?
Define an internal request and response model, then implement provider-specific adapters behind a trait or service interface. Keep provider-only features explicit rather than pretending every provider has identical behavior.
Is streaming always better for agent responses?
No. It can reduce the wait before users see output, but it adds connection-lifecycle and partial-result complexity. Choose it when the client benefits from incremental events and define how errors and cancellation work.
Can a model safely choose which tools to run?
A model can suggest a tool call, but the gateway must validate the request and enforce user permissions, argument constraints, and execution limits before running it.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.