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
sudoprivileges. - 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
OutOfMemoryErroror is killed by the OOM killer. Lower the values inPULSAR_MEMor add RAM. Check withsudo journalctl -u pulsar -n 100anddmesg | grep -i oom. pulsar-adminreturnsConnection refused. The broker is still starting or failed to start. Wait a minute and checksudo systemctl status pulsarand the log in/opt/pulsar/logs/.Tenant not foundorNamespace does not exist. Topic names are case sensitive and must use the fullpersistent://tenant/namespace/topicform. List what exists withpulsar-admin tenants listandpulsar-admin namespaces list acme.- Port 8080 is already in use. Another service (often a Java app or a proxy) listens on 8080. Change
webServicePortin/opt/pulsar/conf/standalone.confand restart the service; remember to pass the new URL topulsar-adminwith--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.
