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 stockprotocwould produce them. -
A blocking service base class,
GreeterGrpc.GreeterImplBase, matching the standardgrpc-javacode generation. -
A Mutiny-flavored variant,
MutinyGreeterGrpc.GreeterImplBase, whose methods returnUni/Multiinstead of usingStreamObservercallbacks. -
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 |
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 |
|
|
Server streaming |
|
|
Client streaming |
|
|
Bidirectional streaming |
|
|
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());
}
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.