apache/iceberg · warning · ServiceUnavailableException

Service unavailable

Error message

Service unavailable: %s

What it means

RESTClient's default error handler maps HTTP 503 responses to ServiceUnavailableException. The catalog service is temporarily unavailable — typically overload, maintenance, or a dependency being down. This is usually transient.

Solutions

  1. Retry the request with exponential backoff and jitter — 503 is expected to be transient
  2. Check the catalog service status/maintenance announcements
  3. Reduce request concurrency in your job (e.g. fewer parallel scan planning calls)
  4. If persistent, contact the catalog operator to check capacity and health

Example fix

// before
Table table = catalog.loadTable(id); // fails on 503 during maintenance
// after
// configure client retry: 'rest.client.retry.max-attempts' etc., or wrap:
Tasks.foreach(() -> catalog.loadTable(id)).retry(5).exponentialBackoff(100, 60000);
Defensive patterns

Strategy: retry

Try / catch

Tasks.foreach(() -> catalog.loadTable(identifier))
    .retry(5)
    .exponentialBackoff(200, 120000)
    .throwFailureWhenFinished();

Prevention

When it happens

Trigger: HTTP 503 returned by the REST server or an intermediate gateway during any catalog request, e.g. during server restart, rolling deployment, or capacity limits.

Common situations: Query engines hammering the catalog during heavy jobs, server maintenance windows, autoscaling cold starts, or upstream dependency (metadata DB) saturation.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/43d4708479053f76. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java:354

      switch (error.code()) {
        case 400:
          if (IllegalArgumentException.class.getSimpleName().equals(error.type())) {
            throw new IllegalArgumentException(error.message());
          }
          throw new BadRequestException("Malformed request: %s", error.message());
        case 401:
          throw new NotAuthorizedException("Not authorized: %s", error.message());
        case 403:
          throw new ForbiddenException("Forbidden: %s", error.message());
        case 405:
        case 406:
          break;
        case 500:
          throw new ServiceFailureException("Server error: %s: %s", error.type(), error.message());
        case 501:
          throw new UnsupportedOperationException(error.message());
        case 503:
          throw new ServiceUnavailableException("Service unavailable: %s", error.message());
      }

      throw createRESTException(error);
    }
  }

  private static class OAuthErrorHandler extends ErrorHandler {
    private static final ErrorHandler INSTANCE = new OAuthErrorHandler();

    @Override
    public ErrorResponse parseResponse(int code, String json) {
      try {
        return OAuthErrorResponseParser.fromJson(code, json);
      } catch (Exception x) {
        LOG.warn("Unable to parse error response", x);
      }
      return ErrorResponse.builder().responseCode(code).withMessage(json).build();
    }

View on GitHub (pinned to 86d9c8fc54)