Skip to content

HTTP Retry Strategies

uni's HTTP client includes automatic retry for transient failures.

Default Retry Behavior

Retry is enabled by default with sensible defaults:

scala
val client = Http.client.newSyncClient

// Automatically retries on transient failures
val response = client.send(request)

Configuring Retries

scala
// Set max retry count
val client = Http.client
  .withConfig(HttpClientConfig().withMaxRetries(5))
  .newSyncClient

// Or per-request
val response = client.withMaxRetry(3).send(request)

Disabling Retry

scala
// Disable for specific request
val response = client.noRetry.send(request)

// Or globally
val config = HttpClientConfig().withMaxRetries(0)

Retryable Conditions

The HTTP client automatically retries on:

ConditionDescription
Connection errorsNetwork unreachable, connection refused
TimeoutsConnect or read timeout
429 Too Many RequestsRate limiting
503 Service UnavailableTemporary outage
502 Bad GatewayUpstream errors
504 Gateway TimeoutUpstream timeout

Non-Retryable Conditions

The following are not retried:

ConditionDescription
4xx Client errors400, 401, 403, 404, etc. (except 429)
5xx Server errors500, 501 (except 502, 503, 504)
SSL errorsCertificate validation failures

Exception Classification

scala
import wvlet.uni.http.HttpExceptionClassifier

// Check if an exception is retryable
HttpExceptionClassifier.isRetryable(exception) match
  case true => // Will be retried
  case false => // Will not be retried

Backoff Strategy

The HTTP client uses exponential backoff:

  1. First retry: ~100ms
  2. Second retry: ~200ms
  3. Third retry: ~400ms
  4. ...with jitter to prevent thundering herd

Handling Max Retry Exceeded

scala
import wvlet.uni.http.HttpMaxRetryException

try
  val response = client.send(request)
catch
  case e: HttpMaxRetryException =>
    logger.error(s"Failed after ${e.retryCount} retries")
    logger.error(s"Last error: ${e.lastException.getMessage}")
    // Use fallback or fail

Custom Retry Logic

For more control, use the control module:

scala
import wvlet.uni.control.Retry

val response = Retry
  .withBackoff(maxRetry = 10)
  .withInitialInterval(500)
  .retryOn {
    case _: IOException => true
    case e: HttpException if e.status.code == 429 => true
    case _ => false
  }
  .run {
    client.noRetry.send(request)
  }

Rate Limiting

Handle rate limits gracefully:

scala
import wvlet.uni.control.Retry

val response = Retry
  .withBackoff(maxRetry = 5)
  .beforeRetry { (e, retryCount) =>
    e match
      case HttpException(status, _) if status.code == 429 =>
        // Wait longer for rate limits
        Thread.sleep(retryCount * 1000)
      case _ => ()
  }
  .run {
    client.send(request)
  }

Combining with Circuit Breaker

scala
import wvlet.uni.control.CircuitBreaker

val breaker = CircuitBreaker(maxFailures = 5)

val response = breaker.protect {
  client.send(request)  // Has built-in retry
}

Best Practices

  1. Use default retry for most cases
  2. Disable for idempotent checks like auth validation
  3. Monitor retry rates to detect service issues
  4. Set reasonable limits to avoid long waits
  5. Log retry attempts for debugging

Released under the Apache 2.0 License.