languagetool-org/languagetool · warning · UnavailableException

Server overloaded, please try again later

Error message

Server overloaded, please try again later

What it means

LanguageTool server executes checks on a bounded pipeline/pool; when all workers are busy the executor rejects the task with RejectedExecutionException, which checkText translates into UnavailableException('Server overloaded, please try again later') — an HTTP 503-style transient condition, not a client bug.

Source

Thrown at languagetool-server/src/main/java/org/languagetool/server/TextChecker.java:630

    int textSize = length;
    List<CheckResults> ruleMatchesSoFar = Collections.synchronizedList(new ArrayList<>());
    Future<List<CheckResults>> future;
    try {
      future = executorService.submit(() -> {
        try (MDC.MDCCloseable c = MDC.putCloseable("rID", LanguageToolHttpHandler.getRequestId(httpExchange))) {
          log.debug("Starting text check on {} chars; params: {}", length, qParams);
          long time = System.currentTimeMillis();
          List<CheckResults> results = getRuleMatches(aText, lang, motherTongue, params, qParams, userConfig, f -> ruleMatchesSoFar.add(new CheckResults(Collections.singletonList(f), Collections.emptyList())));
          log.debug("Finished text check in {}ms. Starting suggestion generation.", System.currentTimeMillis() - time);
          time = System.currentTimeMillis();
          // generate suggestions, otherwise this is not part of the timeout logic and not properly measured in the metrics
          results.stream().flatMap(r -> r.getRuleMatches().stream()).forEach(RuleMatch::computeLazySuggestedReplacements);
          log.debug("Finished suggestion generation in {}ms, returning results.", System.currentTimeMillis() - time);
          return results;
        }
      });
    } catch (RejectedExecutionException e) {
      throw new UnavailableException("Server overloaded, please try again later", e);
    }
    String incompleteResultReason = null;
    List<CheckResults> res;
    Attributes textCheckingAttributes = Attributes.builder()
            .put("text.language", lang.getShortCode())
            .put("text.size", textSize)
            .put("userRules.size", userRules.size())
            .put("dictionary.size", dictWords.size())
            .build();
    Integer finalCount = count;
    Map.Entry<List<CheckResults>, String> resAndReason = TelemetryProvider.INSTANCE.createSpan(SPAN_NAME_PREFIX + "GetRuleMatches", textCheckingAttributes, (span) -> {
        List<CheckResults> localRes;
        String localReason = null;
        try {
          if (limits.getMaxCheckTimeMillis() < 0) {
            localRes = future.get();
          } else {
            localRes = future.get(limits.getMaxCheckTimeMillis(), TimeUnit.MILLISECONDS);

View on GitHub (pinned to 2e990059ce)

Solutions

  1. Retry the request after a short backoff, ideally with exponential backoff and jitter
  2. Reduce request concurrency in the client (serialize or cap parallel checks)
  3. Split large texts to shorten per-request occupancy of the pool
  4. If you operate the server, increase the pipeline pool / maxWorkerThreads configuration or scale horizontally

Example fix

// before
results = Promise.all(texts.map(t => check(t)))  // 50 parallel requests
// after
results = await pLimit(4)(texts.map(t => () => check(t)))  // bounded concurrency + retry on 503
Defensive patterns

Strategy: retry

Validate before calling

// pre-check: keep concurrency below server capacity
const limit = pLimit(Math.max(1, serverConfig.maxWorkers - 1));

Try / catch

async function checkWithRetry(text, tries = 3) {
  for (let i = 0; i < tries; i++) {
    try { return await check(text); }
    catch (e) {
      if ((e.status === 503 || /Server overloaded/.test(e.message)) && i < tries - 1) {
        await sleep(500 * 2 ** i + Math.random() * 250);
        continue;
      }
      throw e;
    }
  }
}

Prevention

When it happens

Trigger: Submitting a /v2/check request while the server's pipeline pool is saturated (more concurrent requests than maxWorkerThreads/pipeline pool size), or after pool rejection from long-running checks hogging threads; hit by all checkText callers.

Common situations: Load spikes, bulk document checks issuing many parallel requests, undersized thread pool relative to traffic, or slow checks (huge texts) blocking the pool.


AI-assisted analysis of languagetool-org/languagetool@2e990059ce (2026-09-06). Data as JSON: /api/errors/1b66cfbb6635fe8d. Report an issue: GitHub.