Skip to content

Configuration

You build a pattern with keyword arguments. You tune it in deployment with environment variables. No code change between the two.

How a value is resolved

A field takes the first of these that supplies it:

  1. A keyword argument. Always wins, always available, needs nothing enabled.
  2. A GREL_* environment variable, unless GREL_ENV_LOAD=0. On by default.
  3. A file, through ExternalConfig, which also reconfigures a running component when the file changes.
  4. The field's default.

The flag gates the GREL_* namespace, which is grelmicro's own. It does not gate a Provider, and it never did. RedisProvider() reads REDIS_URL, PostgresProvider() reads POSTGRES_URL, with no flag, because those names belong to your environment rather than to grelmicro. A provider variable that is missing fails at construction and names the variable it wanted, so there is no silent default to protect you from. Pass env_load=False to a provider to turn its env reads off.

Two GREL_* names sit outside the flag as well, because neither fills a component field. GREL_ENV_LOAD is the flag. GREL_ENVIRONMENT says where the app runs, which decides how a backend that cannot keep a promise across replicas is reported: see the backend check.

GREL_ENV_LOAD is a single process-wide switch. Unset, or set to 1, every GREL_* field is read. Set to 0, none is: a variable that is set is ignored, the default applies, and startup reports it by name:

EnvLoadOffWarning: GREL_LOG_FORMAT is set but was not applied: GREL_ENV_LOAD
turns environment-driven configuration off. Unset it or set GREL_ENV_LOAD=1 to
read the variable, or pass the value directly.

The same report is logged on the grelmicro logger once logging is configured, so on the default backend it also lands in the JSON log stream, where a warning on stderr would be lost.

Step 2 fills every field you did not pass, not only the ones with no default. Pass env_load=False to a component when the values in code are the whole truth. Config resolution has the full contract.

Local development without exporting variables

Two options, and they cover different things.

Put the variables in a .env and load them into the process. Anything that populates os.environ works, and relative paths are fine:

# .env
GREL_LOG_FORMAT=pretty
GREL_LOCK_CART_LEASE_DURATION=60
uv run --env-file .env python -m myapp

Or pass the values in code, which needs no switch at all and is the only option for logging:

from grelmicro.log import configure

configure(format="pretty")

ExternalConfig reads a .env as well, but it feeds reconfigurable components (locks, retries, timeouts, health checks) and not Log, which installs logging once at startup. For log format in local development, use configure(...) or the loaded-environment route above.

Build with keyword arguments

Pass the name first, then the settings:

from grelmicro.coordination import Lock

lock = Lock("cart", lease_duration=60, retry_interval=0.1)

Patterns with variants use factory methods:

from grelmicro.resilience import RateLimiter

api = RateLimiter.sliding_window("api", limit=100, window=60)

That is the whole story for code. Every value lives next to the pattern, easy to read and easy to test.

Tune with environment variables

The deployment overrides any field without touching code. Set the environment variable for the field and grelmicro reads it at startup.

Environment variables are read by default

A GREL_* variable that fills a component field is read unless GREL_ENV_LOAD=0. With GREL_ENV_LOAD=0, a variable that is set is ignored, the default applies, and startup warns with its name. Passing env_load=False to a component turns its reads off and stays quiet. See How a value is resolved for the three ways to configure a component, including a local .env.

The variable name is built from the pattern and the instance name:

GREL_{PATTERN}_{NAME}_{FIELD}

A Lock("cart") reads its lease duration from GREL_LOCK_CART_LEASE_DURATION:

export GREL_LOCK_CART_LEASE_DURATION=120
export GREL_LOCK_CART_RETRY_INTERVAL=0.2

Names with hyphens, dots, slashes, or colons normalise into uppercase segments (payments-eu becomes PAYMENTS_EU, cart.v2 becomes CART_V2).

The instance name becomes the namespace, and the bare GREL_{PATTERN}_ prefix is the default for the whole pattern. Every instance falls back to it, so one variable retunes them all:

export GREL_LOCK_LEASE_DURATION=60   # every Lock in the service

Name an instance to carve out an exception:

export GREL_LOCK_LEASE_DURATION=60          # every Lock
export GREL_LOCK_CHECKOUT_LEASE_DURATION=300   # except this one

That is the whole rule. A twelve-lock service changes one variable, not twelve.

Because the bare prefix is shared, name your instances so their segment cannot start a field name of the same pattern. A Lock("lease") reads GREL_LOCK_LEASE_DURATION for a field named duration, which is the key every lock already reads for lease_duration. This is rare, and renaming the instance is the fix.

A value passed in code wins over both variables. So a hard-coded Lock("cart", lease_duration=60) ignores GREL_LOCK_CART_LEASE_DURATION and GREL_LOCK_LEASE_DURATION. Leave a field out of the constructor to let the deployment set it.

Prefix reference

Pattern Prefix Falls back to
Lock("default") GREL_LOCK_ it is the pattern default
Lock("cart") GREL_LOCK_CART_ GREL_LOCK_
TaskLock("etl") GREL_TASKLOCK_ETL_ GREL_TASKLOCK_
LeaderElection("svc") GREL_LEADERELECTION_SVC_ GREL_LEADERELECTION_
RateLimitFilter() GREL_RATELIMITFILTER_ it is the pattern default
RateLimitFilter(env_name="audit") GREL_RATELIMITFILTER_AUDIT_ GREL_RATELIMITFILTER_
DuplicateFilter() GREL_DUPLICATEFILTER_ it is the pattern default
DuplicateFilter(env_name="audit") GREL_DUPLICATEFILTER_AUDIT_ GREL_DUPLICATEFILTER_
HealthChecks() GREL_HEALTH_ it is the pattern default
log.configure() GREL_LOG_ it is the pattern default
Tasks() GREL_TASK_ it is the pattern default
CachedResponses() GREL_CACHED_RESPONSES_ it is the pattern default
ConditionalRequests() GREL_CONDITIONAL_REQUESTS_ it is the pattern default
IdempotentRequests() GREL_IDEMPOTENT_REQUESTS_ it is the pattern default
RateLimitedRequests() GREL_RATE_LIMITED_REQUESTS_ it is the pattern default
AccessLog() GREL_ACCESS_LOG_ it is the pattern default
JWTVerifier.jwks() GREL_JWTVERIFIER_ it is the pattern default
JWTVerifier.jwks(name="partner") GREL_JWTVERIFIER_PARTNER_ nothing, a verifier reads its own prefix only
ClientBans() GREL_CLIENTBANS_ it is the pattern default

Each pattern page lists its own fields and the exact variable names.

A field holding many values is written as JSON, because that is what a variable can carry and still say where one value ends:

export GREL_ACCESS_LOG_EXCLUDE='["/livez", "/readyz"]'
export GREL_CACHED_RESPONSES_INCLUDE='{"/products/*": 60}'

The same in a mounted file, which is easier to read because the document has lists of its own:

grel:
  access_log:
    exclude: ["/livez", "/readyz"]
  cached_responses:
    include:
      "/products/*": 60

One timezone for the whole service

Most services run on a single wall clock. GREL_TIMEZONE says which one:

export GREL_TIMEZONE=Europe/Zurich

Cron tasks now fire on Zurich wall-clock time, and log timestamps render in Zurich too. It carries no pattern segment, because it belongs to the process rather than to one component.

A component variable is more specific, so it wins. Keep logs on UTC under a Zurich service with:

export GREL_TIMEZONE=Europe/Zurich
export GREL_LOG_TIMEZONE=UTC

The full order for a cron fire time, first one that supplies it:

  1. @tasks.cron(..., timezone="...") on the task.
  2. TaskRouter(timezone="...") on the nearest router that declares one.
  3. Tasks(timezone="...").
  4. GREL_TASK_TIMEZONE.
  5. GREL_TIMEZONE.
  6. "UTC".

Names are IANA names such as Europe/Zurich, in any casing. grelmicro ignores the POSIX TZ variable on purpose. TZ falls back to UTC without complaint when it cannot parse a value, which would turn a typo into a schedule running at the wrong hour. Note that TZ still decides what a naive datetime.now() returns inside your own task bodies.

GREL_TIMEZONE is read once at startup. See Live reconfiguration for what that means for a mounted ConfigMap.

Defaults reference

The most important defaults for operators. All times are in seconds. Override any of them with the pattern prefix above plus the field name, uppercased.

Pattern Field Default
Lock lease_duration 60
Lock retry_interval 0.1
TaskLock lease_duration 60
TaskLock min_hold_duration 1
LeaderElection lease_duration 15
LeaderElection renew_deadline 10
LeaderElection retry_interval 2
LeaderElection backend_timeout 5
TTLCache ttl 60
RateLimiter fail_open False
CircuitBreaker error_threshold 5
CircuitBreaker success_threshold 2
CircuitBreaker reset_timeout 30
Retry attempts 3
Retry backoff.base_delay 0.1
Retry backoff.max_delay 30
Bulkhead max_concurrent None (unbounded)
HealthChecks timeout 5
HealthChecks cache_ttl 1
Idempotency ttl 86400 (1 day)
Tasks timezone UTC
Tasks shutdown_timeout 30

TTLCache sets its ttl in code, not from the environment: it takes no name, so it has no environment namespace of its own. Idempotency does read GREL_IDEMPOTENCY_TTL like every other named pattern. Timeout.seconds, Fallback.when, and Fallback.default are required and have no default.

Advanced

The kwargs-and-env path covers most apps. When you need more, the Advanced configuration page covers:

  • Building from a Pydantic config object with from_config.
  • Composing settings under one pydantic-settings tree.
  • Custom env prefixes with env_prefix= and disabling env reads with env_load=False.
  • The full resolution order contract.

For live reload from a Kubernetes ConfigMap, see Live reconfiguration.