How to Configure Retry Logic and Timeouts for gRPC Client Calls in Kratos Services

To configure retry logic and timeouts for gRPC client calls in Kratos services, use grpc.WithTimeout() for per-request deadlines and either gRPC's native service-config retry policies via grpc.WithOptions() or custom unary interceptors via grpc.WithUnaryInterceptor().

The Kratos framework builds its gRPC client on top of the standard google.golang.org/grpc library, wrapping grpc.Dial with helper utilities located in the transport/grpc package. When you need to configure retry logic and timeouts for gRPC client calls, Kratos provides explicit options in transport/grpc/client.go while allowing direct access to underlying gRPC dial options for advanced scenarios.

Configuring Request Timeouts in Kratos gRPC Clients

Kratos provides the WithTimeout(d time.Duration) option to enforce per-request deadlines. This option is defined in transport/grpc/client.go (lines 51–56) and is applied inside the unaryClientInterceptor function found in transport/grpc/interceptor.go (lines 25–30).

When a call exceeds the specified duration, the interceptor cancels the context with context.DeadlineExceeded. Note that this timeout applies only to the RPC call, not to the underlying connection establishment.

import (
    "context"
    "time"
    
    "github.com/go-kratos/kratos/v2/transport/grpc"
    greeterpb "path/to/your/api/helloworld"
)

func NewGreeterClient() greeterpb.GreeterClient {
    // 5 seconds deadline for every RPC
    conn, _ := grpc.Dial(
        context.Background(),
        grpc.WithEndpoint("localhost:9000"),
        grpc.WithTimeout(5*time.Second),
    )
    return greeterpb.NewGreeterClient(conn)
}

The timeout is injected inside the unary interceptor via ctx = context.WithTimeout(ctx, timeout), ensuring that any blocking RPC operation respects the deadline.

Implementing Retry Logic for gRPC Calls

Kratos does not ship with a built-in retry interceptor for the client side. You have two idiomatic approaches to add retry capabilities: using gRPC's native service configuration or writing a custom unary interceptor.

Option 1: Native gRPC Retry via Service Config

You can enable automatic retries by passing a JSON service configuration through grpc.WithDefaultServiceConfig, which Kratos forwards to the underlying dialer via the WithOptions wrapper (defined in transport/grpc/client.go, lines 100–104).

import (
    "context"
    
    "github.com/go-kratos/kratos/v2/transport/grpc"
    "google.golang.org/grpc"
    greeterpb "path/to/your/api/helloworld"
)

func NewGreeterClientWithRetry() greeterpb.GreeterClient {
    // JSON service config enabling 3 retries with exponential back-off
    const svcConfig = `{
        "methodConfig": [{
            "name": [{"service": "helloworld.Greeter"}],
            "retryPolicy": {
                "MaxAttempts": 4,
                "InitialBackoff": "0.1s",
                "MaxBackoff": "1s",
                "BackoffMultiplier": 2,
                "RetryableStatusCodes": ["UNAVAILABLE","RESOURCE_EXHAUSTED"]
            }
        }]
    }`

    conn, _ := grpc.Dial(
        context.Background(),
        grpc.WithEndpoint("localhost:9000"),
        grpc.WithOptions(
            grpc.WithDefaultServiceConfig(svcConfig),
        ),
    )
    return greeterpb.NewGreeterClient(conn)
}

This approach leverages the gRPC core's built-in retry mechanism, which interprets the policy when the server returns a retryable status code.

Option 2: Custom Unary Interceptor

For custom logic, implement a grpc.UnaryClientInterceptor and register it using WithUnaryInterceptor (handled in transport/grpc/client.go, lines 86–90). This interceptor runs before the built-in timeout interceptor, so the deadline still caps the total elapsed time across all retry attempts.

import (
    "context"
    "time"
    
    "google.golang.org/grpc"
    "github.com/go-kratos/kratos/v2/transport/grpc"
    greeterpb "path/to/your/api/helloworld"
)

// Simple retry interceptor: up to 3 attempts, 200 ms pause
func retryUnaryInterceptor(maxAttempts int, backoff time.Duration) grpc.UnaryClientInterceptor {
    return func(
        ctx context.Context,
        method string,
        req, reply interface{},
        cc *grpc.ClientConn,
        invoker grpc.UnaryInvoker,
        opts ...grpc.CallOption,
    ) error {
        var err error
        for i := 0; i < maxAttempts; i++ {
            err = invoker(ctx, method, req, reply, cc, opts...)
            if err == nil {
                return nil
            }
            // Adapt to your own retryable error list
            time.Sleep(backoff)
        }
        return err
    }
}

func NewClientWithRetry() greeterpb.GreeterClient {
    conn, _ := grpc.Dial(
        context.Background(),
        grpc.WithEndpoint("localhost:9000"),
        grpc.WithUnaryInterceptor(
            retryUnaryInterceptor(3, 200*time.Millisecond),
        ),
        grpc.WithTimeout(5*time.Second),
    )
    return greeterpb.NewGreeterClient(conn)
}

Combining Timeouts and Retries

You can safely combine per-call timeouts with retry policies to ensure that the total time spent on all retry attempts does not exceed your service-level objectives. The timeout interceptor wraps the entire invocation chain, including any retry logic you inject.

conn, _ := grpc.Dial(
    context.Background(),
    grpc.WithEndpoint("localhost:9000"),
    grpc.WithTimeout(3*time.Second),                // Overall call deadline
    grpc.WithOptions(
        grpc.WithDefaultServiceConfig(svcConfig), // Native retry policy
    ),
)

In this configuration, the unaryClientInterceptor applies the deadline first, and the gRPC core handles the retry attempts within that bounded context.

Summary

  • WithTimeout sets per-RPC deadlines via the unaryClientInterceptor in transport/grpc/interceptor.go.
  • WithOptions passes raw grpc.DialOption values (like WithDefaultServiceConfig) directly to the underlying grpc.DialContext.
  • WithUnaryInterceptor allows you to inject custom retry logic that executes before the timeout enforcement.
  • Native gRPC retry policies defined in service config JSON are supported through the WithOptions wrapper.
  • Reference implementations and unit tests are available in transport/grpc/client_test.go.

Frequently Asked Questions

How does Kratos apply the timeout to gRPC calls?

Kratos injects the timeout through the unaryClientInterceptor function in transport/grpc/interceptor.go. This interceptor wraps the RPC invocation with context.WithTimeout(ctx, timeout) before delegating to the invoker, ensuring that any call exceeding the duration is cancelled with context.DeadlineExceeded.

Can I use both native gRPC retry and a custom interceptor together?

Yes. You can pass grpc.WithDefaultServiceConfig via WithOptions for native retries while also supplying a custom interceptor via WithUnaryInterceptor. The custom interceptor executes first, followed by the timeout enforcement, while the native retry policy is handled by the gRPC core at the transport layer.

Does the timeout include connection establishment time?

No. The WithTimeout option applies only to the RPC call itself, not to the underlying connection establishment. For connection timeouts, use WithOptions to pass grpc.WithBlock() and grpc.WithTimeout() dial options directly to the gRPC dialer.

Where are the client options defined in the Kratos source code?

All client configuration options, including WithTimeout, WithUnaryInterceptor, and WithOptions, are defined in transport/grpc/client.go. The timeout enforcement logic resides in transport/grpc/interceptor.go, and usage examples are demonstrated in transport/grpc/client_test.go.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →