Timeouts
By default, JSON-RPC method calls have no timeout — if a method hangs, it ties up a thread indefinitely and the client never receives a response. You can guard against this with a global timeout, per-method timeouts via MicroProfile Fault Tolerance, or both.
Global Timeout
Set a global timeout that applies to all non-streaming methods:
quarkus.json-rpc.method-timeout=30s
Any method that does not complete within this duration receives a timeout error response. Streaming methods (Multi<T> and Flow.Publisher<T>) are not affected — they are long-lived subscriptions where a global timeout does not apply.
The timeout is disabled by default. Use standard Quarkus duration format (30s, 2m, 500ms, etc.).
Per-Method Timeout with MicroProfile Fault Tolerance
For fine-grained control, use the @Timeout annotation from MicroProfile Fault Tolerance. This works because @JsonRPCApi classes are CDI beans, and Quarkus SmallRye Fault Tolerance applies its interceptors through the CDI proxy — exactly as it does for REST endpoints or any other CDI bean.
Add the extension to your project:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
Then annotate individual methods:
import org.eclipse.microprofile.faulttolerance.Timeout;
@JsonRPCApi
public class MyService {
@Timeout(5000) (1)
public String slowOperation() {
return expensiveComputation();
}
public String fastOperation() { (2)
return "instant";
}
}
| 1 | Times out after 5 seconds. |
| 2 | No per-method timeout — uses the global timeout if configured, otherwise no limit. |
Combining with Other Fault Tolerance Annotations
Since the integration works through standard CDI interceptors, all MicroProfile Fault Tolerance annotations work on @JsonRPCApi methods — not just @Timeout. You can combine them as you would on any CDI bean:
@JsonRPCApi
public class ResilientService {
@Timeout(3000)
@Retry(maxRetries = 2)
@Fallback(fallbackMethod = "fallbackLookup")
public String lookup(String id) {
return externalService.fetch(id);
}
String fallbackLookup(String id) {
return cachedData.get(id);
}
}
Combining Global and Per-Method Timeouts
Both mechanisms can be active at the same time. Per-method @Timeout fires first (at the CDI interceptor level, before the router’s timeout), so it takes precedence for methods that have it:
| Method | @Timeout |
Effective timeout |
|---|---|---|
|
|
5 seconds (from Fault Tolerance) |
|
(none) |
30 seconds (from global config) |
The global timeout acts as a safety net — it catches methods that don’t have @Timeout but still hang unexpectedly.
Error Response
When a timeout occurs (from either mechanism), the extension returns a JSON-RPC error with code -32002:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32002,
"message": "Method [MyService#slowOperation] timed out"
}
}
This code is in the JSON-RPC 2.0 server-defined range (-32000 to -32099), alongside the security error codes:
| Code | Name | Meaning |
|---|---|---|
|
Unauthorized |
Authentication required but not provided. |
|
Forbidden |
Authenticated but not authorized. |
|
Timeout |
Method did not complete within the configured time limit. |
Custom Error Handling
If you need custom timeout error responses, you can use a JsonRPCExceptionMapper to intercept the timeout exception:
import io.quarkiverse.jsonrpc.api.JsonRPCError;
import io.quarkiverse.jsonrpc.api.JsonRPCExceptionMapper;
@ApplicationScoped
public class TimeoutMapper implements JsonRPCExceptionMapper {
@Override
public JsonRPCError mapException(Throwable exception) {
if (exception.getClass().getName().contains("TimeoutException")) {
return new JsonRPCError(-50001, "Service is busy, please try again later");
}
return null; // let other mappers or the default handler deal with it
}
}
Exception mappers are consulted before the built-in timeout handling, so your mapper takes precedence.
Notes
-
The global timeout returns an error to the client promptly, but it does not guarantee cancellation of the underlying work. A blocking method may continue running on its worker thread until it completes naturally. This matches the behavior of MicroProfile Fault Tolerance’s
@Timeout. -
Streaming methods (
Multi<T>,Flow.Publisher<T>) are excluded from the global timeout. For streaming, consider using Mutiny’s built-in operators likeMulti.ifNoItem().after(duration).fail()within your method implementation.