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
- Set maxMessageRetries to a positive integer when a deadLetterTopic is configured
- Or remove the deadLetterTopic if infinite retries are intended
- 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
- Always set maxMessageRetries together with deadLetterTopic in the same config builder
- Treat maxMessageRetries < 0 as 'infinite' — never use it as a default sentinel for DLQ configs
- Lint function manifests for DLQ/retry pairing
- Keep DLQ setup in one shared helper that enforces the pairing
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
- DeadLetter topic %s is invalid
- MaxMessageRetries and Effectively once don't gel well
- Either a Java jar or a Python file or a Go executable binary
- Either a Java jar or a Python file or a Go executable binary
- Function Name not provided
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/dffccc7283cb1475.
Report an issue: GitHub.