apache/dubbo · warning · RejectedExecutionException

Number of pending timeouts

Error message

Number of pending timeouts ({}) is greater than or equal to maximum allowed pending timeouts ({})

What it means

RejectedExecutionException thrown by HashedWheelTimer.newTimeout() when the number of pending (not-yet-fired) timeouts exceeds the configured maxPendingTimeouts limit. The pending timeout counter is incremented atomically before the check; if it exceeds the cap, it is decremented back and the exception is thrown. This is a backpressure mechanism to prevent unbounded memory growth from excessive scheduled timeouts. Only enforced when maxPendingTimeouts > 0 (0 or negative means unlimited).

Solutions

  1. Increase maxPendingTimeouts if the workload legitimately requires many concurrent pending timeouts.
  2. Reduce the rate of newTimeout calls or ensure timeouts fire promptly — investigate whether the worker thread is blocked.
  3. Cancel timeouts that are no longer needed (Timeout.cancel()) to free pending slots.
  4. If maxPendingTimeouts is unset (0/negative), the limit is disabled — set it to a value matching your expected concurrency as a safety net.

Example fix

// before — unlimited pending timeouts causes memory risk, or limit too low
new HashedWheelTimer(factory, 100, TimeUnit.MILLISECONDS, 512, 100);
// scheduling 101+ tasks throws

// after — set a higher limit matching expected load
new HashedWheelTimer(factory, 100, TimeUnit.MILLISECONDS, 512, 100000);
// or disable limit entirely (0 or negative)
new HashedWheelTimer(factory, 100, TimeUnit.MILLISECONDS, 512, 0);
Defensive patterns

Strategy: try-catch

Validate before calling

// Check pending count before scheduling (requires access to pendingTimeouts())
if (timer.pendingTimeouts() < maxPending) {
    timer.newTimeout(task, delay, unit);
} else {
    applyBackpressure(); // throttle, queue, or reject at application level
}

Try / catch

try {
    timer.newTimeout(task, delay, unit);
} catch (RejectedExecutionException e) {
    if (e.getMessage().contains("pending timeouts")) {
        logger.warn("Timer pending timeout limit reached, backing off", e);
        backoffAndRetry(task, delay, unit); // exponential backoff
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Scheduling timeouts faster than they fire, accumulating pending timeouts beyond the maxPendingTimeouts limit configured at construction. This indicates the timer is being overwhelmed — tasks are scheduled but not completing (either very long delays or the worker thread is stalled).

Common situations: High-throughput scheduling of short-lived timeouts where the fire rate can't keep up; long delay values causing accumulation; worker thread blocked or slow; misconfigured maxPendingTimeouts that is too low for the workload; a leak where timeouts are scheduled but never cancelled/fired.

Understand the failure class

Related errors


AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14). Data as JSON: /api/errors/6298ca43caff56e7. Report an issue: GitHub.

Appendix: source

Thrown at dubbo-common/src/main/java/org/apache/dubbo/common/timer/HashedWheelTimer.java:396

    @Override
    public boolean isStop() {
        return WORKER_STATE_SHUTDOWN == WORKER_STATE_UPDATER.get(this);
    }

    @Override
    public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (unit == null) {
            throw new NullPointerException("unit");
        }

        long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

        if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
            pendingTimeouts.decrementAndGet();
            throw new RejectedExecutionException("Number of pending timeouts ("
                + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
                + "timeouts (" + maxPendingTimeouts + ")");
        }

        start();

        // Add the timeout to the timeout queue which will be processed on the next tick.
        // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

        // Guard against overflow.
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
    }

View on GitHub (pinned to 3a3043227f)