gRPC Retries and Backoff

Networks fail in small ways all the time. A server restarts, a connection drops, or a request lands at a bad moment. A retry sends the same call again and often succeeds. Done carelessly, retries multiply traffic and crash a struggling server. Done carefully, they hide short glitches from users.

The Phone Call Analogy

You call a friend and hear a busy tone. You wait a moment and call again. If the line stays busy, you wait longer before the next attempt. You never redial a hundred times per second. That patience is called backoff.

Attempt 1 ──✖ fails ── wait 0.1s
Attempt 2 ──✖ fails ── wait 0.2s
Attempt 3 ──✖ fails ── wait 0.4s
Attempt 4 ──✔ success

Which Errors Deserve a Retry

Status CodeRetry?Reason
UNAVAILABLEYesServer or network is temporarily down
DEADLINE_EXCEEDEDSometimesSafe only when the call is idempotent
RESOURCE_EXHAUSTEDCarefullyServer asks for slower traffic
INVALID_ARGUMENTNoThe request itself is wrong
NOT_FOUNDNoRetrying returns the same answer
PERMISSION_DENIEDNoRetrying changes nothing

Idempotency

An idempotent call gives the same result when repeated. Reading a profile is idempotent. Charging a credit card is not, because a retry might charge twice. Retry only safe calls, or add a unique request ID so the server detects duplicates.

message ChargeRequest {
  string idempotency_key = 1;   // server remembers this key
  int64  amount_cents    = 2;
}

The server stores each key with its result. A repeated key returns the stored result and skips a second charge.

Built In Retry Policy

gRPC clients accept a retry policy inside a service config. The policy lives in JSON, and the channel applies it with no extra code in your calls.

{
  "methodConfig": [{
    "name": [{ "service": "shop.OrderService" }],
    "retryPolicy": {
      "maxAttempts": 4,
      "initialBackoff": "0.1s",
      "maxBackoff": "1s",
      "backoffMultiplier": 2,
      "retryableStatusCodes": ["UNAVAILABLE"]
    }
  }]
}

Policy Fields

  • maxAttempts counts the first try plus retries. gRPC caps the value at 5.
  • initialBackoff sets the first wait.
  • backoffMultiplier grows each following wait.
  • maxBackoff limits the longest wait.
  • retryableStatusCodes lists the codes that trigger a retry.

Attach the Policy in Go

const serviceConfig = `{
  "methodConfig": [{
    "name": [{ "service": "shop.OrderService" }],
    "retryPolicy": {
      "maxAttempts": 4,
      "initialBackoff": "0.1s",
      "maxBackoff": "1s",
      "backoffMultiplier": 2,
      "retryableStatusCodes": ["UNAVAILABLE"]
    }
  }]
}`

conn, err := grpc.NewClient("orders:50051",
  grpc.WithTransportCredentials(creds),
  grpc.WithDefaultServiceConfig(serviceConfig),
)

Jitter

Imagine a thousand clients that all fail at the same second. If each waits exactly 100 milliseconds, all thousand retry together and hit the server in one wave. Jitter adds a random delay to each wait. The built in policy applies jitter automatically, which spreads retries across time.

Retry Throttling

A server in trouble gets worse when every client retries. Retry throttling stops retries when too many calls fail. The client keeps a token bucket. Failures drain tokens, successes refill them, and an empty bucket blocks further retries.

Deadlines Still Rule

All attempts share one overall deadline. Retries never extend the total time. A call with a two second deadline stops retrying at two seconds, whatever the policy says.

Hedging

Hedging sends a second copy of a slow call before the first one finishes. The client takes the first reply and cancels the rest. Hedging cuts tail latency for idempotent reads, and it increases server load. Use it sparingly.

Summary

Retry transient errors such as UNAVAILABLE, wait longer between attempts, add jitter, and respect deadlines. Protect non idempotent calls with idempotency keys. Retry throttling prevents a small failure from growing into an outage.