apache/pulsar · error · IllegalArgumentException

Dead Letter Topic specified, however max retries is set to i

Error message

Dead Letter Topic specified, however max retries is set to infinity

What it means

A dead-letter topic only receives messages after a finite number of retries; if maxMessageRetries is null or negative (meaning infinite retries), messages would never reach the DLQ, so the combination is rejected. doCommonChecks throws this IllegalArgumentException when a deadLetterTopic is set but max retries is effectively infinite.

Source

Thrown at pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java:876

        if (functionConfig.getTimeoutMs() != null && functionConfig.getTimeoutMs() <= 0) {
            throw new IllegalArgumentException("Function timeout must be a positive number");
        }

        if (functionConfig.getTimeoutMs() != null
                && functionConfig.getProcessingGuarantees() != null
                && functionConfig.getProcessingGuarantees() != FunctionConfig.ProcessingGuarantees.ATLEAST_ONCE) {
            throw new IllegalArgumentException("Message timeout can only be specified with processing guarantee is "
                    + FunctionConfig.ProcessingGuarantees.ATLEAST_ONCE.name());
        }

        if (functionConfig.getMaxMessageRetries() != null && functionConfig.getMaxMessageRetries() >= 0
                && functionConfig.getProcessingGuarantees() == FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE) {
            throw new IllegalArgumentException("MaxMessageRetries and Effectively once don't gel well");
        }
        if ((functionConfig.getMaxMessageRetries() == null || functionConfig.getMaxMessageRetries() < 0)
                && !org.apache.commons.lang3.StringUtils.isEmpty(functionConfig.getDeadLetterTopic())) {
            throw new IllegalArgumentException("Dead Letter Topic specified, however max retries is set to infinity");
        }
        if (functionConfig.getRetainKeyOrdering() != null
                && functionConfig.getRetainKeyOrdering()
                && functionConfig.getProcessingGuarantees() != null
                && functionConfig.getProcessingGuarantees() == FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE) {
            throw new IllegalArgumentException(
                    "When effectively once processing guarantee is specified, retain Key ordering cannot be set");
        }
        if (functionConfig.getRetainKeyOrdering() != null && functionConfig.getRetainKeyOrdering()
                && functionConfig.getRetainOrdering() != null && functionConfig.getRetainOrdering()) {
            throw new IllegalArgumentException("Only one of retain ordering or retain key ordering can be set");
        }

        if (!isEmpty(functionConfig.getPy()) && !org.apache.pulsar.common.functions.Utils
                .isFunctionPackageUrlSupported(functionConfig.getPy())
                && functionConfig.getPy().startsWith(BUILTIN)) {
            String filename = functionConfig.getPy();
            if (filename.contains("..")) {

View on GitHub (pinned to 820761864e)

Solutions

  1. Set maxMessageRetries to a positive integer when a deadLetterTopic is configured
  2. Or remove the deadLetterTopic if infinite retries are intended
  3. Check config-merging code so setting DLQ always accompanies retries

Example fix

// before
config.setDeadLetterTopic("persistent://public/default/dlq");
// after
config.setDeadLetterTopic("persistent://public/default/dlq");
config.setMaxMessageRetries(3);
Defensive patterns

Strategy: validation

Validate before calling

if (config.getDeadLetterTopic() != null && !config.getDeadLetterTopic().isEmpty()
        && (config.getMaxMessageRetries() == null || config.getMaxMessageRetries() < 0)) {
    throw new IllegalArgumentException("DLQ requires finite maxMessageRetries");
}

Type guard

boolean dlqRetriesPairing(FunctionConfig c) {
    boolean hasDlq = c.getDeadLetterTopic() != null && !c.getDeadLetterTopic().isEmpty();
    boolean finiteRetries = c.getMaxMessageRetries() != null && c.getMaxMessageRetries() >= 0;
    return !hasDlq || finiteRetries;
}

Try / catch

try {
    admin.functions().createFunction(functionConfig, sourceConfigLocation);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("max retries is set to infinity")) {
        config.setMaxMessageRetries(3); // or remove DLQ
    } else { throw e; }
}

Prevention

When it happens

Trigger: createFunction/updateFunction where getDeadLetterTopic() is non-empty and (getMaxMessageRetries() == null || getMaxMessageRetries() < 0) — e.g. --dead-letter-topic set via CLI without --max-message-retries, or maxMessageRetries explicitly -1 in YAML.

Common situations: CLI scripts adding a DLQ flag but omitting the retries flag; manifests copied from examples that set deadLetterTopic only; confusion that setting -1 means 'use default' rather than 'infinite'.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/dffccc7283cb1475. Report an issue: GitHub.