Coming from Quarkus
SmallRye Health, MicroProfile Fault Tolerance, the scheduler, the cache and quarkus-oidc all have a grelmicro equivalent. The biggest difference: grelmicro can keep a circuit breaker's and a rate limit's state in Redis, Valkey or PostgreSQL, so every replica shares it. This page maps each concept, says where the behavior differs, and lists what grelmicro leaves to other libraries.
Quick reference
| Quarkus | grelmicro | Page |
|---|---|---|
@Liveness, @Readiness, /q/health |
@health.check(..., liveness=True) for /livez, @health.check(...) for /readyz and /healthz |
Health |
| Extension health checks | auto_health, one check per provider, off by default |
Health |
@Retry |
@retry(when=..., attempts=...) |
Retry |
@Timeout |
Timeout(name, seconds=...) |
Timeout |
@CircuitBreaker |
CircuitBreaker(name) |
Circuit Breaker |
@Bulkhead |
Bulkhead(name, max_concurrent=...) |
Bulkhead |
@Fallback |
@fallback(when=..., default=...) |
Fallback |
SmallRye @RateLimit |
RateLimiter.sliding_window(...), RateLimiter.token_bucket(...) |
Rate Limiter |
| Several fault tolerance annotations on one method | Stack(...) |
Composition |
@Scheduled(every = ...), @Scheduled(cron = ...) |
@tasks.every(interval=...), @tasks.cron("...") |
Task Scheduler |
| Quartz clustered mode | gate="claim" on a task |
Task Scheduler |
@CacheResult, @CacheInvalidate |
@cached(cache), cache.delete(...) |
@cached |
Micrometer, @Timed |
Metrics(), @measure |
Metrics |
OpenTelemetry, @WithSpan |
Trace(), @instrument |
Tracing |
quarkus-oidc bearer tokens |
AuthenticatedRequests with a JWTVerifier |
Authentication |
@PermissionsAllowed("orders_read") |
Authenticated(scopes=["orders_read"]) |
Authentication |
quarkus-oidc-client |
OAuthClient with ClientCredentials |
Tokens |
@QuarkusTest, @InjectMock |
TestClient(app) with micro.fake(), micro.override(...) |
Testing |
Concept differences
Shared state across replicas
A MicroProfile circuit breaker and a SmallRye rate limit keep their state in one JVM, per bean class and method. A grelmicro CircuitBreaker or RateLimiter keeps it in the backend, by name. On Redis, Valkey or PostgreSQL every replica sees the same breaker and spends the same budget. SQLite shares it between the processes of one host only. A grelmicro limiter also takes a key=, such as one budget per user.
Retry counts calls
@Retry(maxRetries = 4) retries every Exception and makes up to 5 calls. @retry(attempts=5) makes up to 5 calls too, and when= is required: name the exceptions worth retrying.
The circuit breaker counts consecutive failures
@CircuitBreaker opens on a failure ratio over the last requestVolumeThreshold calls. grelmicro opens after error_threshold consecutive failures, 5 by default, and closes after 2 successful probes.
One worker per run without Quartz
concurrentExecution = SKIP only covers one instance, and running a job once across the cluster takes Quartz with a JDBC store. grelmicro's gate="claim" does it through a backend every replica reaches, such as Redis or PostgreSQL, and claimed and local tasks sit side by side in one Tasks.
Health has three fixed endpoints
/livez reflects the liveness checks, /readyz runs the critical checks, and /healthz runs the rest. There are no health groups and no startup endpoint. A check with critical=False stays out of readiness. A Quarkus liveness check, such as a deadlock detector, is a check with liveness=True under HealthChecks(liveness=Liveness(...)). It drives /livez, and a worker that keeps failing it exits so it is replaced. See Catch a stuck worker.
Every route is protected by default
Quarkus endpoints are open unless annotated, or unless deny-unannotated-endpoints is set. Once AuthenticatedRequests is registered, grelmicro requires a token on every route. Open a route with Anonymous() or exclude=. grelmicro checks scopes. There are no roles: read the groups claim with Claims in the handler and answer 403 when you need one.
The outbox needs no broker
The Debezium outbox extension writes the event in your transaction, and a CDC connector publishes it to a broker. grelmicro writes the message in your transaction too, then a relay in the same service hands it to your handler, with retries and a dead-letter state.
Context lives on the call
MDC is a map for each thread. grelmicro context follows the call across await. micro.install(app) opens one for each request and message, and @instrument and span() open one for a block, so add_context in a route handler adds its fields to every log record until the response. Outside all of them, add_context does nothing.
Side by side
Fault tolerance on one call
@Retry(maxRetries = 2)
@Timeout(1000)
@CircuitBreaker(requestVolumeThreshold = 4)
@Fallback(fallbackMethod = "fallbackRecommendations")
public List<Coffee> recommendations(int id) {
// ...
}
import httpx
from grelmicro.resilience import (
Bulkhead,
CircuitBreaker,
Fallback,
RateLimiter,
Retry,
Stack,
Timeout,
)
SYSTEM = "recs-api"
CALL = "recs-list"
# Named for the system, so every call site shares one circuit and one budget.
breaker = CircuitBreaker(SYSTEM)
limiter = RateLimiter.token_bucket(SYSTEM, capacity=20, refill_rate=10)
pool = Bulkhead(SYSTEM, max_concurrent=10)
recs_list = Stack(
CALL,
patterns=[
# Named for this call: its default and its deadline are its own.
Fallback(CALL, when=Exception, default=[]),
Retry.exponential(CALL, when=httpx.HTTPError, attempts=3),
breaker,
limiter(key_template="user:{user_id}"),
pool,
Timeout(CALL, seconds=1.0),
],
)
@recs_list
async def get_recommendations(
client: httpx.AsyncClient, user_id: str
) -> list[str]:
response = await client.get(f"/recs/{user_id}")
response.raise_for_status()
return response.json()
A job that runs on one node
@Transactional
@Scheduled(every = "60s", identity = "task-job")
void schedule() {
Task task = new Task();
task.persist();
}
quarkus.quartz.clustered=true
quarkus.quartz.store-type=jdbc-cmt
from grelmicro.task import Tasks
tasks = Tasks()
@tasks.every(interval=60, gate="claim")
async def cleanup():
print("Running cleanup...")
A readiness check
@Readiness
@ApplicationScoped
public class DatabaseConnectionHealthCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.up("Database connection health check");
}
}
from fastapi import FastAPI
from grelmicro import Grelmicro
from grelmicro.health import HealthChecks, HealthDetails
from grelmicro.integrations.fastapi import health_router
health = HealthChecks()
@health.check("database")
async def check_database() -> HealthDetails | None:
return None
micro = Grelmicro(uses=[health])
app = FastAPI()
micro.install(app)
app.include_router(health_router())
# Endpoints: GET /livez, GET /readyz, GET /healthz
A protected API
@PermissionsAllowed("orders_read")
@GET
@Path("/order")
public List<Order> listOrders() {
return List.of(new Order("1"));
}
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.audience=orders-api
from fastapi import FastAPI
from grelmicro import Grelmicro
from grelmicro.http import AuthenticatedRequests, ErrorResponses
from grelmicro.integrations.fastapi import (
Anonymous,
Authenticated,
CurrentPrincipal,
)
from grelmicro.security import JWTVerifier
micro = Grelmicro(
uses=[
ErrorResponses(),
AuthenticatedRequests(
JWTVerifier.discover(
"https://auth.example.com/", audience="orders-api"
),
exclude=("/livez", "/readyz"),
),
]
)
app = FastAPI()
@app.get("/orders")
async def list_orders(principal: CurrentPrincipal) -> dict[str, str | None]:
return {"caller": principal.subject}
@app.delete(
"/orders/{order_id}",
dependencies=[Authenticated(scopes=["orders:write"])],
)
async def cancel(order_id: int) -> dict[str, int]:
return {"cancelled": order_id}
@app.get("/catalog", dependencies=[Anonymous()])
async def catalog() -> list[str]:
return ["tea", "coffee"]
micro.install(app)
False friends
| Word | In Quarkus | In grelmicro |
|---|---|---|
| profile | A config profile such as %prod. |
A Shield preset such as internal or api. GREL_ENVIRONMENT only names the deployment tier and selects no configuration |
| extension | A build-time module that generates code | No build step. Framework support lives in grelmicro.integrations and wires at runtime |
| Dev Services | Real services started in containers for dev and test | No containers. micro.fake() swaps grelmicro's backends to memory |
| bean | A container-managed object with injection | No beans. A component has a start and a stop |
| health groups | Endpoints for a named set of checks | No groups. critical=False and the ?exclude= query are the levers |
| reactive | Mutiny and Vert.x | Plain async and await on asyncio |
| management interface | Health and metrics on port 9000 | OpsServer, for a process that serves no HTTP |
No equivalent, do this instead
- Config profiles: keep one environment file per environment and load it with your runner. See Configuration.
- Dev Services: start containers with testcontainers-python in a pytest fixture, or use
micro.fake()when the test is about your code. - Health groups and the startup probe: use
critical=False,?exclude=, and pointstartupProbeat/livez. - A failure-ratio circuit breaker: grelmicro has the consecutive count only.
- Roles with
@RolesAllowed: use scopes, or read the claim withClaimsin the handler and answer403. - The Vert.x event bus: call the function. Use the Outbox for work after a commit, and FastStream for a broker.
- Panache and Hibernate: use SQLAlchemy or SQLModel.
Outbox.publishtakes theirAsyncSession.