Apache Pulsar is a distributed messaging and streaming platform that separates stateless brokers from storage (Apache BookKeeper), and organizes topics into tenants and namespaces for multi-tenant use. In this tutorial you will install Pulsar 4.0 LTS in standalone mode on Ubuntu 24.04, run it as a systemd service, create a tenant, a namespace and topics with pulsar-admin, and send and receive messages from the command line and from Python.

Standalone mode runs a broker, a BookKeeper bookie and the metadata store in a single JVM. It is the right choice for development, CI and small internal workloads; a production cluster with several brokers and bookies follows the same concepts but is out of scope here.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 vCPUs, 4 GB of RAM and 20 GB of free disk.
  • A non-root user with sudo privileges.
  • Basic familiarity with the Linux command line.

Pulsar standalone has authentication disabled by default. Keep ports 6650 (binary protocol) and 8080 (admin REST API) closed to the Internet; this guide only uses them from localhost.

Step 1 - Installing Java

Pulsar 4.0 requires Java 17 or newer. Install the headless OpenJDK 21 runtime from the Ubuntu repositories:

sudo apt update
sudo apt install openjdk-21-jre-headless

Check the installed version:

java -version
openjdk version "21.0.8" 2025-07-15
OpenJDK Runtime Environment (build 21.0.8+9-Ubuntu-0ubuntu124.04.1)
OpenJDK 64-Bit Server VM (build 21.0.8+9-Ubuntu-0ubuntu124.04.1, mixed mode, sharing)

The exact patch version will differ; any 21.x is fine.

Step 2 - Downloading and installing Pulsar

Pulsar runs best under its own unprivileged system user. Create a pulsar user without a login shell:

sudo useradd --system --home-dir /opt/pulsar --shell /usr/sbin/nologin pulsar

Download the binary release and its checksum to /tmp. This guide uses 4.0.13, the current release of the 4.0 LTS line; check the Pulsar download page and adjust PULSAR_VERSION if a newer 4.0.x is available:

PULSAR_VERSION=4.0.13
cd /tmp
curl -fLO "https://downloads.apache.org/pulsar/pulsar-${PULSAR_VERSION}/apache-pulsar-${PULSAR_VERSION}-bin.tar.gz"
curl -fLO "https://downloads.apache.org/pulsar/pulsar-${PULSAR_VERSION}/apache-pulsar-${PULSAR_VERSION}-bin.tar.gz.sha512"

Verify the archive before extracting it:

sha512sum -c "apache-pulsar-${PULSAR_VERSION}-bin.tar.gz.sha512"
./apache-pulsar-4.0.13-bin.tar.gz: OK

Extract it into /opt, point a stable /opt/pulsar symlink at the versioned directory, and give the pulsar user ownership. Pulsar writes its data to data/ and its logs to logs/ inside this directory:

sudo tar -xzf "apache-pulsar-${PULSAR_VERSION}-bin.tar.gz" -C /opt
sudo ln -sfn "/opt/apache-pulsar-${PULSAR_VERSION}" /opt/pulsar
sudo chown -R pulsar:pulsar "/opt/apache-pulsar-${PULSAR_VERSION}"

The symlink makes upgrades simple: extract the new version, move the data/ directory across, and repoint /opt/pulsar.

Step 3 - Running Pulsar as a systemd service

By default Pulsar reserves a 2 GB heap plus up to 4 GB of direct memory, which is too much for a 4 GB server. The PULSAR_MEM environment variable overrides those values. Create a systemd unit:

sudo nano /etc/systemd/system/pulsar.service

Add the following content:

[Unit]
Description=Apache Pulsar (standalone)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pulsar
Group=pulsar
WorkingDirectory=/opt/pulsar
Environment="PULSAR_MEM=-Xms1g -Xmx1g -XX:MaxDirectMemorySize=1g"
ExecStart=/opt/pulsar/bin/pulsar standalone
Restart=on-failure
RestartSec=10
LimitNOFILE=65536
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target

On servers with 8 GB of RAM or more you can raise the heap to 2 GB and direct memory to 2 GB or remove the Environment line to use the defaults.

Reload systemd and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now pulsar

The first start takes 30 to 60 seconds while BookKeeper initializes its storage. Follow the log until the broker reports that it is ready:

sudo journalctl -u pulsar -f

Press Ctrl+C once you see a log line from PulsarService containing messaging service is ready.

To avoid typing the full path to the Pulsar tools, add them to your PATH for the current session (append the line to ~/.bashrc to make it permanent):

export PATH="/opt/pulsar/bin:$PATH"

Confirm that the broker answers on the admin API:

pulsar-admin brokers healthcheck
pulsar-admin clusters list
ok
"standalone"

The standalone cluster is always named standalone; you will reference that name when creating tenants.

Step 4 - Creating a tenant, a namespace and topics

Pulsar topics have fully qualified names of the form persistent://tenant/namespace/topic. A tenant is an administrative unit (a team or a customer), and a namespace groups topics that share policies such as retention.

Create a tenant called acme that is allowed to use the standalone cluster:

pulsar-admin tenants create acme --allowed-clusters standalone

Create a namespace for order events inside it:

pulsar-admin namespaces create acme/orders

By default Pulsar deletes messages as soon as every subscription has acknowledged them. Set a retention policy so acknowledged messages are kept for 7 days or up to 5 GB, whichever limit is reached first:

pulsar-admin namespaces set-retention acme/orders --time 7d --size 5G
pulsar-admin namespaces get-retention acme/orders
{
  "retentionTimeInMinutes" : 10080,
  "retentionSizeInMB" : 5120
}

Create a partitioned topic with four partitions, which lets several consumers process messages in parallel:

pulsar-admin topics create-partitioned-topic persistent://acme/orders/new-orders --partitions 4
pulsar-admin topics list-partitioned-topics acme/orders
persistent://acme/orders/new-orders

Non-partitioned topics do not need to be created in advance: with the default standalone configuration, Pulsar creates them automatically the first time a producer or consumer uses them.

Step 5 - Producing and consuming from the command line

The pulsar-client tool is the quickest way to check that messages flow end to end. Open a second SSH session and start a consumer on a subscription called cli-test. -n 0 keeps it running until you stop it:

/opt/pulsar/bin/pulsar-client consume persistent://acme/orders/new-orders -s cli-test -n 0

In the first session, publish ten messages:

pulsar-client produce persistent://acme/orders/new-orders -m "order created" -n 10
... 10 messages successfully produced

The consumer window prints each message as it arrives:

----- got message -----
key:[null], properties:[], content:order created

Stop the consumer with Ctrl+C. The subscription remains on the broker, so you can inspect it and its backlog:

pulsar-admin topics partitioned-stats persistent://acme/orders/new-orders

In the JSON output, msgInCounter shows the messages received by the topic and the subscriptions.cli-test.msgBacklog field shows how many messages are still waiting to be acknowledged, which should be 0.

Step 6 - Using Pulsar from Python

Applications talk to Pulsar through client libraries. Install Python's virtual environment support and create an environment for the official pulsar-client package:

sudo apt install python3-venv
python3 -m venv ~/pulsar-demo
~/pulsar-demo/bin/pip install pulsar-client

Create a producer script:

nano ~/producer.py
import json

import pulsar

client = pulsar.Client("pulsar://localhost:6650")
producer = client.create_producer("persistent://acme/orders/new-orders")

for order_id in range(1, 6):
    payload = json.dumps({"order_id": order_id, "status": "created"})
    producer.send(payload.encode("utf-8"))
    print(f"sent order {order_id}")

client.close()

Create a consumer script that uses a Shared subscription. With Shared, several copies of the script can run at once and Pulsar distributes messages between them; each message must be acknowledged, or it is redelivered:

nano ~/consumer.py
import json

import pulsar

client = pulsar.Client("pulsar://localhost:6650")
consumer = client.subscribe(
    "persistent://acme/orders/new-orders",
    subscription_name="order-processor",
    consumer_type=pulsar.ConsumerType.Shared,
)

try:
    while True:
        try:
            msg = consumer.receive(timeout_millis=10000)
        except pulsar.Timeout:
            print("no messages for 10 seconds, exiting")
            break
        order = json.loads(msg.data())
        print(f"processing order {order['order_id']}")
        consumer.acknowledge(msg)
finally:
    client.close()

Start the consumer first so the subscription exists before messages are published (a new subscription starts at the latest message by default):

~/pulsar-demo/bin/python ~/consumer.py

In another session, run the producer:

~/pulsar-demo/bin/python ~/producer.py

The consumer prints the five orders and exits ten seconds after the last one:

processing order 1
processing order 2
processing order 3
processing order 4
processing order 5
no messages for 10 seconds, exiting

Other subscription types are Exclusive (the default, one consumer only), Failover (one active consumer with standbys) and Key_Shared (parallel consumers that keep per-key ordering).

Troubleshooting

  • The service restarts in a loop with OutOfMemoryError or is killed by the OOM killer. Lower the values in PULSAR_MEM or add RAM. Check with sudo journalctl -u pulsar -n 100 and dmesg | grep -i oom.
  • pulsar-admin returns Connection refused. The broker is still starting or failed to start. Wait a minute and check sudo systemctl status pulsar and the log in /opt/pulsar/logs/.
  • Tenant not found or Namespace does not exist. Topic names are case sensitive and must use the full persistent://tenant/namespace/topic form. List what exists with pulsar-admin tenants list and pulsar-admin namespaces list acme.
  • Port 8080 is already in use. Another service (often a Java app or a proxy) listens on 8080. Change webServicePort in /opt/pulsar/conf/standalone.conf and restart the service; remember to pass the new URL to pulsar-admin with --admin-url http://localhost:NEW_PORT.

Conclusion

You now have Apache Pulsar 4.0 running in standalone mode under systemd, with a tenant, a namespace with a retention policy, a partitioned topic, and working producers and consumers from the CLI and Python. From here you can enable token authentication before exposing the broker to other hosts, export the broker metrics at http://localhost:8080/metrics/ to Prometheus, or move to a multi-node deployment with separate brokers and bookies when you need high availability.