apache/pulsar · error · IllegalArgumentException

Output topic %s is also being used as an input topic (topics

Error message

Output topic %s is also being used as an input topic (topics must be one or the other)

What it means

A Pulsar function's output topic must be distinct from all of its input topics; a topic can only serve one role for a given function. verifyNoTopicClash (invoked from doCommonChecks during function config validation) throws this error when the configured output topic also appears in the input topic collection, because such a wiring would create a self-feeding loop and violate Pulsar's one-role-per-topic rule.

Source

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

    private static void doGolangChecks(FunctionConfig functionConfig) {
        if (functionConfig.getProcessingGuarantees() == FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE) {
            throw new RuntimeException("Effectively-once processing guarantees not yet supported in Go function");
        }

        if (functionConfig.getWindowConfig() != null) {
            throw new IllegalArgumentException("Windowing is not supported in Go function yet");
        }

        if (functionConfig.getMaxMessageRetries() != null && functionConfig.getMaxMessageRetries() >= 0) {
            throw new IllegalArgumentException("Message retries not yet supported in Go function");
        }
    }

    private static void verifyNoTopicClash(Collection<String> inputTopics, String outputTopic)
            throws IllegalArgumentException {
        if (inputTopics.contains(outputTopic)) {
            throw new IllegalArgumentException(
                    String.format(
                            "Output topic %s is also being used as an input topic (topics must be one or the other)",
                            outputTopic));
        }
    }

    public static void doCommonChecks(FunctionConfig functionConfig) {
        if (isEmpty(functionConfig.getTenant())) {
            throw new IllegalArgumentException("Function tenant cannot be null");
        }
        if (isEmpty(functionConfig.getNamespace())) {
            throw new IllegalArgumentException("Function namespace cannot be null");
        }
        if (isEmpty(functionConfig.getName())) {
            throw new IllegalArgumentException("Function name cannot be null");
        }
        // go doesn't need className. Java className is done in doJavaChecks.
        if (functionConfig.getRuntime() == FunctionConfig.Runtime.PYTHON) {

View on GitHub (pinned to 820761864e)

Solutions

  1. Change the output topic to a different topic name not present in the input topic list
  2. If round-tripping is intended, use two distinct topics (e.g. input-topic and input-topic-processed) and chain functions
  3. Remove the overlapping topic from the input list if it was only meant to be the output

Example fix

// before
config.setInputSpecs(Map.of("persistent://public/default/events", ConsumerConfig.builder().build()));
config.setOutputTopic("persistent://public/default/events");

// after
config.setInputSpecs(Map.of("persistent://public/default/events", ConsumerConfig.builder().build()));
config.setOutputTopic("persistent://public/default/events-processed");
Defensive patterns

Strategy: validation

Validate before calling

Set<String> inputs = new HashSet<>(config.getInputTopics() != null ? config.getInputTopics() : List.of());
if (config.getInputSpecs() != null) inputs.addAll(config.getInputSpecs().keySet());
String output = config.getOutputTopic();
if (output != null && inputs.contains(output)) {
    throw new IllegalArgumentException("Output topic " + output + " must not also be an input topic");
}
FunctionConfigUtils.validateFunctionConfig(config, null);

Type guard

boolean topicsAreDisjoint(FunctionConfig c) {
    Set<String> inputs = new HashSet<>();
    if (c.getInputTopics() != null) inputs.addAll(c.getInputTopics());
    if (c.getInputSpecs() != null) inputs.addAll(c.getInputSpecs().keySet());
    return c.getOutputTopic() == null || !inputs.contains(c.getOutputTopic());
}

Try / catch

try {
    FunctionConfigUtils.validateFunctionConfig(config, null);
} catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().contains("is also being used as an input topic")) {
        throw new ConfigException("Fix topic wiring: " + e.getMessage()); // surface actionable message to operator
    }
    throw e;
}

Prevention

When it happens

Trigger: Creating/updating any function where getOutputTopic() equals one of getInputTopics() (or inputTopicsSpec / custom serde input topic names), e.g. pulsar-admin functions create --inputs topic-a --output topic-a, or a config object with overlapping input/output topic sets.

Common situations: Copy-paste mistakes where output was left identical to input; auto-generated configs using the same topic name variable; pipelines wiring feedback loops incorrectly; renaming refactors that aliased both fields to one topic.

Related errors


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