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:
- A keyword argument. Always wins, always available, needs nothing enabled.
- A
GREL_*environment variable, whenGREL_ENV_LOADis truthy. Off by default. - A file, through
ExternalConfig, which also reconfigures a running component when the file changes. - 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.
Step 2 is the one that surprises people. GREL_ENV_LOAD is a single
process-wide switch, so a variable set without it is ignored and the default
applies. That situation reports at startup instead of passing silently, naming
the variable:
UserWarning: GREL_LOG_FORMAT is set but was not applied: environment-driven
configuration is opt-in. Set GREL_ENV_LOAD=1 to enable it, 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. Deployment covers where to set the
switch in a container.
The switch exists because step 2 fills every field you did not pass, not only the ones with no default. An app that passes some settings from its own config object and leaves the rest would otherwise split one component's configuration across two sources without saying so. Opting in makes that a decision rather than an accident. 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_ENV_LOAD=1
GREL_LOG_FORMAT=PRETTY
GREL_LOCK_CART_LEASE_DURATION=60
uv run --env-file .env python -m myapp
GREL_ENV_LOAD=1 has to be in the file too, or nothing else in it is read.
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 opt-in
A GREL_* variable that fills a component field is read only when
GREL_ENV_LOAD is truthy (1, true, yes, on), the flag itself and
GREL_ENVIRONMENT excepted. Without it the variable is ignored and the
default applies.
Setting one while the flag is unset warns at startup, so the mistake is not
silent. Passing env_load=False is a deliberate opt-out 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 |
Each pattern page lists its own fields and the exact variable names.
One timezone for the whole service
Most services run on a single wall clock. GREL_TIMEZONE says which one:
export GREL_ENV_LOAD=1
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:
@tasks.cron(..., timezone="...")on the task.TaskRouter(timezone="...")on the nearest router that declares one.Tasks(timezone="...").GREL_TASK_TIMEZONE.GREL_TIMEZONE."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-settingstree. - Custom env prefixes with
env_prefix=and disabling env reads withenv_load=False. - The full resolution order contract.
For live reload from a Kubernetes ConfigMap, see Live reconfiguration.