Pulsar subscriptions are more than just queues; they’re sophisticated mechanisms for consumers to reliably ingest messages from topics, and the type you choose fundamentally alters how your consumers interact with the data.

Let’s see this in action. Imagine we have a topic named persistent://public/default/my-topic and we want to consume messages.

First, a quick setup for a producer and a consumer.

# Start a Pulsar standalone instance (if you don't have one running)
# pulsar standalone

# Create a topic (if it doesn't exist)
# pulsar-admin topics create persistent://public/default/my-topic

# Produce some messages
# pulsar-client produce persistent://public/default/my-topic --messages "message1,message2,message3"

Now, let’s look at the subscription types.

Exclusive Subscription

This is the simplest and most restrictive. Only one consumer can attach to an exclusive subscription at any given time. If a second consumer tries to connect, it will be rejected.

How it works: Pulsar guarantees that each message will be delivered to exactly one consumer. This is ideal for scenarios where you need to process each message only once, like in a single worker processing a batch of tasks.

Example Consumer (Java):

import org.apache.pulsar.client.api.*;

public class ExclusiveConsumer {
    public static void main(String[] args) throws Exception {
        PulsarClient client = PulsarClient.builder()
                .serviceUrl("pulsar://localhost:6650")
                .build();

        Consumer<String> consumer = client.newConsumer(Schema.STRING)
                .topic("persistent://public/default/my-topic")
                .subscriptionName("my-exclusive-subscription")
                .subscribe();

        System.out.println("Exclusive consumer started. Waiting for messages...");

        while (true) {
            Message<String> msg = consumer.receive();
            System.out.println("Received message: " + msg.getValue() + " from subscription " + consumer.getSubscriptionName());
            consumer.acknowledge(msg);
        }
    }
}

If you try to run another instance of ExclusiveConsumer with the same my-exclusive-subscription name, it will fail with an error like: Consumer is already connected to subscription my-exclusive-subscription.

Shared Subscription

This is where Pulsar gets interesting for scaling. Multiple consumers can attach to a shared subscription. Pulsar then distributes messages round-robin across all connected consumers.

How it works: Each message is still delivered to exactly one consumer, but that consumer can be any of the ones attached to the shared subscription. This allows for parallel processing of messages. If a consumer disconnects, Pulsar will re-deliver any unacknowledged messages it held to other consumers.

Example Consumer (Java):

import org.apache.pulsar.client.api.*;

public class SharedConsumer {
    public static void main(String[] args) throws Exception {
        PulsarClient client = PulsarClient.builder()
                .serviceUrl("pulsar://localhost:6650")
                .build();

        Consumer<String> consumer = client.newConsumer(Schema.STRING)
                .topic("persistent://public/default/my-topic")
                .subscriptionName("my-shared-subscription")
                .subscriptionType(SubscriptionType.Shared) // Crucial difference!
                .subscribe();

        System.out.println("Shared consumer started. Waiting for messages...");

        while (true) {
            Message<String> msg = consumer.receive();
            System.out.println("Received message: " + msg.getValue() + " from subscription " + consumer.getSubscriptionName() + " by consumer " + consumer.hashCode());
            consumer.acknowledge(msg);
        }
    }
}

Now, you can run multiple instances of SharedConsumer. You’ll see messages being distributed among them. If one consumer dies, the other consumers will eventually pick up its unacknowledged messages.

Failover Subscription

This type provides redundancy. Multiple consumers can attach, but only one is active at a time. If the active consumer fails, one of the other consumers takes over.

How it works: Pulsar ensures that messages are delivered to exactly one consumer. The consumers are ordered by priority (or connection order, depending on configuration). The highest priority consumer becomes the active one and receives all messages. If it disconnects, the next highest priority consumer becomes active. This is perfect for high-availability scenarios where you need a hot standby.

Example Consumer (Java):

import org.apache.pulsar.client.api.*;

public class FailoverConsumer {
    public static void main(String[] args) throws Exception {
        PulsarClient client = PulsarClient.builder()
                .serviceUrl("pulsar://localhost:6650")
                .build();

        Consumer<String> consumer = client.newConsumer(Schema.STRING)
                .topic("persistent://public/default/my-topic")
                .subscriptionName("my-failover-subscription")
                .subscriptionType(SubscriptionType.Failover) // Crucial difference!
                .subscribe();

        System.out.println("Failover consumer started. Waiting for messages...");

        while (true) {
            Message<String> msg = consumer.receive();
            System.out.println("Received message: " + msg.getValue() + " from subscription " + consumer.getSubscriptionName() + " by consumer " + consumer.hashCode() + " (Active)");
            consumer.acknowledge(msg);
        }
    }
}

If you run multiple FailoverConsumer instances, only one will receive messages. The others will be in a standby state. If you stop the active consumer, one of the standbys will immediately become active and start receiving messages.

The core distinction across these types lies in how Pulsar manages message distribution and consumer availability. Exclusive is for single-process, guaranteed-once delivery. Shared is for distributed, parallel processing where any consumer can handle any message. Failover is for high availability, ensuring a single active consumer with automatic failover.

What most people overlook is that when using Shared or Failover subscriptions, Pulsar doesn’t just distribute messages; it manages active cursors for each connected consumer. When a consumer disconnects, Pulsar effectively "pauses" its cursor, and if it reconnects, it resumes from where it left off, preserving message ordering per consumer. This cursor management is key to Pulsar’s reliability.

Understanding these subscription types is fundamental to designing robust and scalable messaging applications with Pulsar, as the choice directly impacts throughput, availability, and processing guarantees.

Next, you’ll likely want to explore how Pulsar handles message acknowledgements and redelivery policies for each of these subscription types.

Want structured learning?

Take the full Pulsar course →