Skip to main content

Set up Java client

Use the combined Java dependency for applications using the v4 client, v5 client, the admin API, or any combination of them. The combined artifacts require Java 17 or later. Choose Java 25 LTS for running new applications when you have the choice.

Changing dependencies and changing client APIs are separate choices. Both combined artifacts support the existing v4 API (org.apache.pulsar.client.api), the v5 API (org.apache.pulsar.client.api.v5), and the admin API. You can update the dependency while keeping your v4 application code. Existing v4 applications using regular topics do not need to change their API or dependencies for a broker upgrade. Use this setup when configuring or updating application dependencies.

Step 1: Install Java client library​

Use pulsar-client-v5-all by default for new dependency configurations. It is an unshaded aggregate that resolves the client and admin implementations transitively. If conflicts in the unshaded dependency graph cannot be resolved, pulsar-client-v5-shaded is a fallback with relocated implementation dependencies.

ArtifactDependency graph
pulsar-client-v5-allUnshaded v4 client, v5 client, and admin implementations, with their transitive dependencies
pulsar-client-v5-shadedOne JAR containing relocated v4 client, v5 client, and admin implementations and bundled third-party dependencies
Strongly recommended: align dependencies and exclude conflicting clients

To avoid classpath conflicts and incompatible libraries:

Complete dependency configuration​

For copyable build files with both BOMs and conflict handling, see Java dependency configuration:

Step 2: Connect to Pulsar cluster​

To connect to Pulsar using client libraries, you need to specify a Pulsar protocol URL.

You can assign Pulsar protocol URLs to specific clusters and use the pulsar scheme. The following is an example of localhost with the default port 6650:

pulsar://localhost:6650

If you have multiple brokers, separate IP:port by commas:

pulsar://localhost:6550,localhost:6651,localhost:6652

If you use mTLS authentication, add +ssl in the scheme:

pulsar+ssl://pulsar.us-west.example.com:6651

Java client Performance considerations​

Increasing the memory limit​

For high-throughput applications, tune the amount of memory with the v4 Java client builder's memoryLimit configuration option. The default limit is 64 MiB. Choose a limit that fits your workload and the application's available memory. To share a limit across several clients, see Share resources across client instances.

By default Java applications have a limit for direct memory allocations. The allocations are limited by the -XX:MaxDirectMemorySize JVM option. In many JVM implementations, this defaults to the maximum heap size unless explicitly set. Allocations happen outside of the Java heap.

Pending producer queues when the memory limit is disabled​

A v4 producer with no explicit message-count limits normally relies on the client memory limit. If you disable that memory limit with memoryLimit(0, SizeUnit.BYTES), an unset maxPendingMessages instead resolves to 1000; an unset maxPendingMessagesAcrossPartitions normally resolves to 50000. These defaults keep producers from silently losing their queue bounds when the memory cap is removed.

An explicit value takes precedence, including maxPendingMessages(0), which removes the message-count bound even when the client memory limit is disabled. An explicitly configured positive across-partition budget can lower the per-partition queue limit to its share of that budget, with at least one pending message per partition. The implicit fallback budget is not divided across partitions and does not lower a per-partition limit that the application explicitly set.

v4 producers fail sends when a queue is full by default; blockIfQueueFull(true) makes them wait for capacity. v5 producers use a client memory budget without these v4 pending-message settings; see Bound pending sends.

Recommended JVM options​

The following JVM options let the Pulsar Java client and Netty use their optimized code paths. The client works without them, but falls back to slower implementations. The sections below explain what each option does.

JVM optionJava versionWhy it's needed
--add-opens java.base/java.nio=ALL-UNNAMEDAllNetty's optimized direct memory buffer access
--add-opens java.base/jdk.internal.misc=ALL-UNNAMEDAllNetty's optimized direct memory buffer access
-Dio.netty.tryReflectionSetAccessible=trueAllNetty's optimized direct memory buffer access
--sun-misc-unsafe-memory-access=allow23 and laterNetty's use of sun.misc.Unsafe
--add-opens java.base/java.util.zip=ALL-UNNAMEDAllOptimized checksum calculation when the native library isn't available
--enable-native-access=ALL-UNNAMED24 and laterLoading Netty's native libraries without warnings

The options with a minimum Java version must only be used on that version or later, as the JVM doesn't start with an option that it doesn't recognize. For example, on Java 25:

java \
--add-opens java.base/java.nio=ALL-UNNAMED \
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED \
--add-opens java.base/java.util.zip=ALL-UNNAMED \
--sun-misc-unsafe-memory-access=allow \
--enable-native-access=ALL-UNNAMED \
-Dio.netty.tryReflectionSetAccessible=true \
-jar my-application.jar

Use -Dorg.apache.pulsar.shade.io.netty.tryReflectionSetAccessible=true instead of -Dio.netty.tryReflectionSetAccessible=true for the shaded client fallback, since Netty is relocated in it.

Enabling optimized Netty direct memory buffer access​

The Pulsar Java client uses Netty under the hood and uses Netty direct buffers for data transport.

Netty has a feature that allows optimized direct memory buffer access. This feature enables Netty to use low level APIs such as sun.misc.Unsafe for direct memory operations, which provides faster allocation and deallocation of direct buffers. The faster deallocation can help avoid direct memory exhaustion and java.lang.OutOfMemoryError: Direct buffer memory errors. These errors can occur when the Netty memory pool and memory allocator cannot release memory back to the operating system quickly enough.

To enable this feature, Netty needs reflective access to JDK internals, so you need to add the following JVM options to the application that uses the Java client:

  • --add-opens java.base/java.nio=ALL-UNNAMED
  • --add-opens java.base/jdk.internal.misc=ALL-UNNAMED

In addition, you need to add one of the following JVM options:

  • -Dorg.apache.pulsar.shade.io.netty.tryReflectionSetAccessible=true for the shaded client fallback
  • -Dio.netty.tryReflectionSetAccessible=true for the unshaded client

Allowing Netty to use sun.misc.Unsafe​

The memory access methods of sun.misc.Unsafe are deprecated for removal, and the JDK is restricting them in stages. On Java 25 and later, Netty doesn't use sun.misc.Unsafe unless it's allowed, and the optimized direct memory buffer access isn't available even with the options above.

Add --sun-misc-unsafe-memory-access=allow on Java 23 and later to keep it available, and to avoid the warnings that the JDK otherwise prints about these methods. Java versions before 23 don't have this option.

Enabling optimized checksum calculation when native library loading fails​

The Pulsar Java client uses com.scurrilous.circe.checksum.Crc32cIntChecksum class from the BookKeeper client for checksum calculation. For optimized checksum calculation Pulsar attempts to load libcirce-checksum native library. When that isn't available, such as on platforms other than Linux x86_64, com.scurrilous.circe.checksum.Java9IntHash class is used, which calls the JDK's CRC32C implementation directly through a private method.

Although it calls the method through a MethodHandle, it has to make the method accessible first, so this only works when --add-opens java.base/java.util.zip=ALL-UNNAMED is passed in the JVM options. Otherwise, the client falls back to the slower com.scurrilous.circe.checksum.Java8IntHash class, with the following error message:

Unable to use reflected methods:
java.lang.reflect.InaccessibleObjectException: Unable to make private static int java.util.zip.CRC32C.updateBytes(int,byte[],int,int) accessible: module java.base does not "opens java.util.zip" to unnamed module

Allowing native access​

Netty loads its native libraries, such as those for the epoll and io_uring transports, with java.lang.System::loadLibrary, which is a restricted method as of Java 24. Add --enable-native-access=ALL-UNNAMED on Java 24 and later to avoid the warnings that the JDK otherwise prints. The JDK will block restricted methods in a future release unless native access is enabled.

GraalVM native images​

The unshaded v4 client and admin implementations embed native-image reflection, resource, and runtime-initialization metadata in pulsar-client-original and pulsar-client-admin-original. GraalVM Native Image consumes this metadata from the JARs' META-INF/native-image directories.

Use the unshaded artifacts for this configuration: its class names refer to the unshaded client implementation. The embedded configuration does not establish native-image compatibility for the relocated classes in the shaded artifacts. Follow the dependency-alignment guidance above when using the unshaded clients.

The Pulsar source includes native-image smoke tests for v4 Java string-message production/consumption and admin operations in tests/pulsar-client-native-image. Validate the features your application uses, including schemas and authentication plugins; application-specific classes accessed through reflection can require additional reachability metadata.