Skip to content

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 point startupProbe at /livez.
  • A failure-ratio circuit breaker: grelmicro has the consecutive count only.
  • Roles with @RolesAllowed: use scopes, or read the claim with Claims in the handler and answer 403.
  • 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.publish takes their AsyncSession.