gRPC

This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against.

Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change.

This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Quarkus treats gRPC as a first-class citizen alongside REST: a .proto file is the single source of truth, Quarkus generates the Java client/server stubs from it at build time, and the same reactive foundations used elsewhere in the framework (Mutiny, virtual threads, unified HTTP/2) apply to gRPC services without a separate programming model to learn. This page covers protobuf-first development, the three ways to implement a service (blocking, Mutiny, virtual-thread), the four streaming shapes, and the cross-cutting concerns — interceptors, status codes, health/reflection, TLS, and testing — needed to run gRPC services in production.

Protobuf-first development

Add the quarkus-grpc extension to a project:

./mvnw quarkus:add-extension -Dextensions=grpc

which adds the following to pom.xml (shown for reference; add-extension writes it for you):

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-grpc</artifactId>
</dependency>

Place .proto files under src/main/proto. Quarkus wires the protobuf-maven-plugin/protoc toolchain into the build automatically — there is no manual protoc invocation to run in the common case, and no generated-sources directory to check in. A minimal Greeter service with a single SayHello RPC:

// src/main/proto/greeter.proto
syntax = "proto3";

option java_package = "org.acme.grpc";
option java_outer_classname = "GreeterProto";

package greeter;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

On the next build (including a dev-mode hot replace), Quarkus generates, under target/generated-sources/grpc/org/acme/grpc/:

  • Plain protobuf message classes (HelloRequest, HelloReply) and their builders, exactly as stock protoc would produce them.

  • A blocking service base class, GreeterGrpc.GreeterImplBase, matching the standard grpc-java code generation.

  • A Mutiny-flavored variant, MutinyGreeterGrpc.GreeterImplBase, whose methods return Uni/Multi instead of using StreamObserver callbacks.

  • A typed client stub usable with @GrpcClient (see below).

The exact set of generated artifacts and the options that control them (message-only generation, cross-project stub sharing, etc.) are documented in detail upstream; see Quarkus Guides — gRPC Code Generation Reference rather than re-deriving them here.

Nothing above is Quarkus-specific to the .proto syntax itself — it is standard proto3. What Quarkus adds is the automatic, build-integrated generation step and the Mutiny-based stub alongside the plain blocking one.

Implementing services

Blocking implementation

Implementing the generated blocking base class directly is the simplest style, and is a good fit for handlers that call blocking APIs (JDBC, a blocking HTTP client) and do not need explicit control over which thread pool they run on:

package org.acme.grpc;

import io.quarkus.grpc.GrpcService;
import io.grpc.stub.StreamObserver;

@GrpcService
public class GreeterService extends GreeterGrpc.GreeterImplBase {

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        String message = "Hello " + request.getName();
        responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build());
        responseObserver.onCompleted();
    }
}

@GrpcService is the gRPC equivalent of a JAX-RS resource class: it marks the bean as the implementation Quarkus wires into the gRPC server for that generated base class. Only one @GrpcService bean may implement a given service.

Mutiny implementation

Implementing the Mutiny-flavored base class instead returns Uni<Response> for a single result or Multi<Response> for a stream, avoiding StreamObserver callbacks entirely — consistent with how reactive REST endpoints are written elsewhere in a Quarkus application. See Reactive Programming and Mutiny for Uni/Multi fundamentals; this page only covers how they map onto gRPC:

package org.acme.grpc;

import io.quarkus.grpc.GrpcService;
import io.smallrye.mutiny.Uni;

@GrpcService
public class GreeterService extends MutinyGreeterGrpc.GreeterImplBase {

    @Override
    public Uni<HelloReply> sayHello(HelloRequest request) {
        return Uni.createFrom().item(() ->
                HelloReply.newBuilder().setMessage("Hello " + request.getName()).build());
    }
}

Running blocking code on a virtual thread

A handler implemented against the blocking base class can be annotated @RunOnVirtualThread so Quarkus dispatches each call on a JDK virtual thread instead of the platform (event-loop-adjacent) worker pool, letting blocking calls inside it park cheaply instead of occupying a limited pooled thread:

package org.acme.grpc;

import io.quarkus.grpc.GrpcService;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.grpc.stub.StreamObserver;

@GrpcService
public class GreeterService extends GreeterGrpc.GreeterImplBase {

    @Override
    @RunOnVirtualThread
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        // blocking JDBC/HTTP calls here park the virtual thread instead of
        // blocking a limited platform worker thread
        String message = "Hello " + request.getName();
        responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build());
        responseObserver.onCompleted();
    }
}

@RunOnVirtualThread does not apply to a Mutiny-style method returning Uni/Multi — those already run non-blocking pipelines on the event loop by design. See Virtual Threads for the mechanism and pitfalls (pinning, synchronized blocks) in more depth than this page repeats.

Consuming a gRPC service: @GrpcClient

Any CDI bean can inject a typed client stub for a service with @GrpcClient, matching the client’s configured name to the properties shown in the next section:

package org.acme.grpc;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.grpc.GrpcClient;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class GreeterClient {

    @GrpcClient("greeter")
    MutinyGreeterGrpc.MutinyGreeterStub greeter;

    public Uni<String> greet(String name) {
        return greeter.sayHello(HelloRequest.newBuilder().setName(name).build())
                .map(HelloReply::getMessage);
    }
}

with the client’s target configured in application.properties:

quarkus.grpc.clients.greeter.host=localhost
quarkus.grpc.clients.greeter.port=9000

The four streaming shapes

gRPC supports four RPC shapes, distinguished by whether the request and the response are a single message or a stream of messages. All four are declared in the .proto file with the stream keyword and implemented with the matching Uni/Multi combination on the Mutiny stub (a Multi parameter or return type standing in for a stream):

Shape .proto declaration Mutiny signature

Unary

rpc SayHello (HelloRequest) returns (HelloReply);

Uni<HelloReply> sayHello(HelloRequest request)

Server streaming

rpc SayHelloStream (HelloRequest) returns (stream HelloReply);

Multi<HelloReply> sayHelloStream(HelloRequest request)

Client streaming

rpc SayHelloBatch (stream HelloRequest) returns (HelloReply);

Uni<HelloReply> sayHelloBatch(Multi<HelloRequest> requests)

Bidirectional streaming

rpc Chat (stream HelloRequest) returns (stream HelloReply);

Multi<HelloReply> chat(Multi<HelloRequest> requests)

A server-streaming implementation, returning multiple responses for one request:

package org.acme.grpc;

import io.quarkus.grpc.GrpcService;
import io.smallrye.mutiny.Multi;
import java.time.Duration;

@GrpcService
public class GreeterService extends MutinyGreeterGrpc.GreeterImplBase {

    @Override
    public Multi<HelloReply> sayHelloStream(HelloRequest request) {
        return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
                .onOverflow().drop()
                .map(tick -> HelloReply.newBuilder()
                        .setMessage("Hello " + request.getName() + " #" + tick)
                        .build());
    }
}

and a bidirectional-streaming implementation, echoing each inbound message as it arrives while the client is still sending:

    @Override
    public Multi<HelloReply> chat(Multi<HelloRequest> requests) {
        return requests.map(request ->
                HelloReply.newBuilder().setMessage("Echo: " + request.getName()).build());
    }
flowchart LR subgraph Unary["Unary: 1 request -> 1 response"] direction LR U1[Client] -->|1 request| U2[Server] U2 -->|1 response| U1 end subgraph ServerStreaming["Server streaming: 1 request -> N responses"] direction LR S1[Client] -->|1 request| S2[Server] S2 -->|response 1| S1 S2 -->|response 2| S1 S2 -->|response N| S1 end subgraph ClientStreaming["Client streaming: N requests -> 1 response"] direction LR C1[Client] -->|request 1| C2[Server] C1 -->|request 2| C2 C1 -->|request N| C2 C2 -->|1 response| C1 end subgraph Bidi["Bidirectional streaming: N requests <-> N responses, concurrently"] direction LR B1[Client] -->|request 1| B2[Server] B2 -->|response 1| B1 B1 -->|request 2| B2 B2 -->|response 2| B1 end

Interceptors

ServerInterceptor (from grpc-java) intercepts every incoming call before it reaches a service method, and is the gRPC equivalent of a JAX-RS filter — used for cross-cutting concerns such as authentication header extraction, request logging, or injecting a correlation ID into the call context:

package org.acme.grpc;

import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.quarkus.grpc.GlobalInterceptor;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
@GlobalInterceptor
public class LoggingInterceptor implements ServerInterceptor {

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        System.out.println("Incoming call to " + call.getMethodDescriptor().getFullMethodName());
        return next.startCall(call, headers);
    }
}

@GlobalInterceptor applies the interceptor to every gRPC service in the application; omitting it and instead registering the bean only on a specific @GrpcService class scopes it to that service. Client-side interceptors follow the same ClientInterceptor contract and are attached to a @GrpcClient stub through configuration rather than by annotating the injection point.

Error handling and status codes

gRPC does not use HTTP status codes or Java exceptions on the wire — every call completes with a Status, one of a fixed set of gRPC status codes (OK, NOT_FOUND, INVALID_ARGUMENT, PERMISSION_DENIED, FAILED_PRECONDITION, INTERNAL, UNAVAILABLE, and others). An application exception should be translated to the status that best matches its meaning rather than left to surface as an opaque INTERNAL:

package org.acme.grpc;

import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.quarkus.grpc.GrpcService;
import io.smallrye.mutiny.Uni;

@GrpcService
public class GreeterService extends MutinyGreeterGrpc.GreeterImplBase {

    @Override
    public Uni<HelloReply> sayHello(HelloRequest request) {
        if (request.getName().isBlank()) {
            return Uni.createFrom().failure(
                    Status.INVALID_ARGUMENT.withDescription("name must not be blank")
                            .asRuntimeException());
        }
        return Uni.createFrom().item(() ->
                HelloReply.newBuilder().setMessage("Hello " + request.getName()).build());
    }
}

A Uni/Multi that fails with a plain (non-gRPC) exception is mapped by Quarkus to Status.INTERNAL automatically; failing explicitly with a StatusRuntimeException (as above) is how a handler chooses a more specific code instead.

Health checking and server reflection

Quarkus can expose the standard gRPC health-checking service and the server reflection service alongside the application’s own services, both useful with a generic gRPC CLI client such as grpcurl that has no compiled stubs of its own:

quarkus.grpc.server.use-separate-server=false
grpc-health-check=true

With reflection enabled, grpcurl can discover and call a service without a local copy of the .proto file:

grpcurl -plaintext localhost:9000 list
grpcurl -plaintext localhost:9000 greeter.Greeter/SayHello -d '{"name": "Quarkus"}'
grpcurl -plaintext localhost:9000 grpc.health.v1.Health/Check

TLS and mTLS

A gRPC server (or client) channel is secured the same way a REST endpoint or REST client is: by naming a TLS configuration registered under quarkus.tls.* rather than pointing directly at keystore/truststore files inline. For mutual TLS, the server’s configuration additionally requires and validates a client certificate:

quarkus.tls.grpc-tls.key-store.p12.path=server-keystore.p12
quarkus.tls.grpc-tls.key-store.p12.password=changeit
quarkus.tls.grpc-tls.trust-store.p12.path=server-truststore.p12
quarkus.tls.grpc-tls.trust-store.p12.password=changeit

quarkus.grpc.server.tls-configuration-name=grpc-tls
quarkus.grpc.server.ssl.client-auth=REQUIRED

and a client channel is pointed at the matching client-side TLS configuration under quarkus.grpc.clients.<name>.tls-configuration-name. This is the same named TLS registry concept used to secure a REST client — see that page’s TLS section rather than re-documenting the registry itself here.

Unified port vs. a dedicated gRPC port

By default, Quarkus runs gRPC on the same port as HTTP/1.1 traffic (quarkus.http.port, 8080 by default), multiplexing gRPC and REST requests over the same HTTP/2-capable listener — the simplest option, and the right default for most deployments since it means one port to open, load-balance, and secure.

A dedicated gRPC port is still available when a deployment genuinely needs gRPC and HTTP separated — for example to apply different network policies to each, or because a load balancer in front of the service only understands plain HTTP/1.1 and would otherwise break gRPC’s HTTP/2 framing:

quarkus.grpc.server.use-separate-server=true
quarkus.grpc.server.port=9000
quarkus.grpc.server.host=0.0.0.0

Dev UI and CLI tooling

Running quarkus dev (see Developer Experience) surfaces a gRPC card in the Dev UI (/q/dev) listing every discovered service and RPC method, with a form to invoke a method directly from the browser — the gRPC equivalent of the REST endpoint browser’s "invoke from the browser" form, without needing grpcurl or a separate client during local development.

Deploying to Kubernetes

gRPC’s use of HTTP/2 framing means a couple of details deserve attention beyond what a plain HTTP/1.1 REST service needs: the Kubernetes Service/container port should be named or annotated so the ingress/proxy in front of it negotiates HTTP/2 rather than falling back to HTTP/1.1, and if a dedicated gRPC port is used (see above) it needs its own Service port entry alongside the HTTP one. See Kubernetes and OpenShift for the extension and manifest mechanics this only assumes here.

Testing gRPC services

@QuarkusTest starts the application (including its gRPC server) the same way it does for REST endpoints, and a test can inject a @GrpcClient stub directly to exercise a service in-process, without a network round trip to a separately started process:

package org.acme.grpc;

import io.quarkus.grpc.GrpcClient;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import org.junit.jupiter.api.Test;

@QuarkusTest
class GreeterServiceTest {

    @GrpcClient("greeter")
    MutinyGreeterGrpc.MutinyGreeterStub greeter;

    @Test
    void sayHelloReturnsGreeting() {
        HelloReply reply = greeter.sayHello(HelloRequest.newBuilder().setName("Quarkus").build())
                .subscribe().withSubscriber(UniAssertSubscriber.create())
                .awaitItem()
                .getItem();

        org.junit.jupiter.api.Assertions.assertEquals("Hello Quarkus", reply.getMessage());
    }
}

For depth on @QuarkusTest fixtures, injection, and native-image testing that this section does not repeat, see Testing.