Function Tool Performance Optimization: Timeout, Concurrency, and Resource Management

A Tool with slow response makes the entire Agent slow. This article covers three optimization directions: timeout control, concurrent calls, connection pool management, making your Agent run robustly in real production environments.

Problem: Slow Tools Drag Down Entire Agent

By default, Tool’s Call() is synchronous. If one Tool takes 10 seconds to return, the entire Agent waits 10 seconds.

User request → Agent → Tool A (3s) → Tool B (10s) → Return to user
                  Entire Agent stuck here

1. Timeout Control: Don’t Let Any Tool Block Agent

Basic Timeout Implementation

func (myTool) Call(ctx context.Context, input string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    result := make(chan string, 1)
    errCh := make(chan error, 1)

    go func() {
        // Actual work logic
        r, err := doHeavyWork()
        if err != nil {
            errCh <- err
        } else {
            result <- r
        }
    }()

    select {
    case r := <-result:
        return r, nil
    case err := <-errCh:
        return "", err
    case <-ctx.Done():
        return "", fmt.Errorf("tool execution timeout after 5s")
    }
}

Global Default Timeout

Configure global timeout when creating Agent:

config := &launcher.Config{
    AgentLoader: agent.NewSingleLoader(myAgent),
    Timeout:     30 * time.Second,  // Global timeout 30 seconds
}

Tool-Level Timeout Configuration

Put timeout in Tool’s configuration:

type configurableTool struct {
    timeout time.Duration
}

func (t configurableTool) Call(ctx context.Context, input string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, t.timeout)
    defer cancel()
    // ...
}

2. Concurrent Calls: Run Multiple Tools Simultaneously

If a request needs to call multiple independent Tools, concurrent execution saves time:

Serial: Tool A (2s) + Tool B (3s) + Tool C (4s) = 9s
Parallel: [Tool A (2s), Tool B (3s), Tool C (4s)] = 4s

ADK Go’s Concurrent Tool Calls

ADK Go itself supports calling multiple Tools simultaneously. Just describe in Instruction that parallel calls are OK:

Instruction: "You can call multiple tools simultaneously to fetch information, no need to wait for one to complete before calling the next."

Model automatically decides which Tools can be called in parallel.

Concurrency Within Tool for Batch Data

If one Tool needs to handle multiple requests (e.g., batch book queries):

func (batchTool) Call(ctx context.Context, input string) (string, error) {
    var args struct {
        Titles []string `json:"titles"`
    }
    json.Unmarshal([]byte(input), &args)

    // Concurrent queries
    var wg sync.WaitGroup
    results := make([]string, len(args.Titles))

    for i, title := range args.Titles {
        wg.Add(1)
        go func(idx int, t string) {
            defer wg.Done()
            results[idx] = queryBook(t)
        }(i, title)
    }

    wg.Wait()
    return strings.Join(results, "\n"), nil
}

3. Connection Pool Management: Reduce TCP Connection Overhead

Tools often need to call external APIs. Creating new connections each call has handshake overhead. Connection pool reuses connections.

HTTP Client Connection Pool

type httpTool struct {
    client *http.Client
}

func newHTTPTool() *httpTool {
    return &httpTool{
        client: &http.Client{
            Transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 10,
                IdleConnTimeout:     90 * time.Second,
            },
            Timeout: 10 * time.Second,
        },
    }
}

func (t *httpTool) Call(ctx context.Context, input string) (string, error) {
    req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.example.com/query", strings.NewReader(input))
    resp, err := t.client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    // ...
}

Database Connection Pool

If Tool queries database:

import "database/sql"
import _ "github.com/go-sql-driver/mysql"

type dbTool struct {
    db *sql.DB
}

func newDBTool() *dbTool {
    db, _ := sql.Open("mysql", "user:password@tcp(localhost:3306)/mydb?parseTime=true")
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)
    return &dbTool{db: db}
}

4. Resource Limits: Prevent Tool from Exhausting System Resources

Concurrency Limit

Use semaphore to limit concurrent executions:

var (
    maxConcurrent = 10
    semaphore     = make(chan struct{}, maxConcurrent)
)

func (t *myTool) Call(ctx context.Context, input string) (string, error) {
    select {
    case semaphore <- struct{}{}:
        defer func() { <-semaphore }()
    case <-ctx.Done():
        return "", fmt.Errorf("too many concurrent requests")
    }
    // Execute actual logic
}

Memory Limit

Go’s runtime can monitor goroutine count:

import "runtime"

func checkResources() error {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    if m.NumGoroutine > 10000 {
        return fmt.Errorf("too many goroutines: %d", m.NumGoroutine)
    }
    return nil
}

5. Monitoring and Debugging: Know How Tools Are Performing

Log Tool Execution Time

func (t *myTool) Call(ctx context.Context, input string) (string, error) {
    start := time.Now()
    result, err := doWork()
    duration := time.Since(start)

    if err != nil {
        log.Printf("Tool %s failed after %v: %v", t.Name(), duration, err)
    } else {
        log.Printf("Tool %s succeeded in %v", t.Name(), duration)
    }
    return result, err
}

Metrics Reporting

Integrate Prometheus:

import "github.com/prometheus/client_golang/prometheus"

var toolDuration = prometheus.NewHistogramVec(
    prometheus.NewHistogramOpts{
        Name:    "tool_execution_duration_seconds",
        Help:    "Tool execution duration",
        Buckets: []float64{0.1, 0.5, 1, 3, 5, 10},
    },
    []string{"tool_name", "status"},
)

func init() {
    prometheus.Register(toolDuration)
}

func (t *myTool) Call(ctx context.Context, input string) (string, error) {
    start := time.Now()
    result, err := doWork()

    toolDuration.WithLabelValues(t.Name(), statusFromError(err)).Observe(time.Since(start).Seconds())
    return result, err
}

Next Steps

Performance optimization done. Next, MCP—Model Context Protocol, ADK Go’s mainstream way of connecting external services, can connect any service that exposes MCP interface to Agent.

Function Tool Basics | MCP Server Integration →


Follow “Mengshou Programming” on WeChat for more Go ADK hands-on tutorials, weekly updates on Go / AI programming 实战干货.