Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Environment Configuration

Every service that runs shove in production ends up writing the same small parser: read ORDERS_MAX_CONSUMERS, trim it, treat an empty string as unset, fall back to a default, range-check the result, and produce an error that actually names the variable when someone typos a deployment manifest.

The optional env-config feature owns that shape so your service doesn't have to.

[dependencies]
shove = { version = "0.x", features = ["env-config"] }

It is std-only and adds no dependencies. Services that configure shove in code pay nothing for it.

Two rules that hold everywhere

Variables are prefix-scoped. shove never claims a bare name like MAX_CONSUMERS. One process routinely runs several consumer groups that need to be tuned independently, so every reader takes a prefix and looks up {PREFIX}_{KEY}.

Unset means "keep the default"; invalid means "fail". A missing, empty, or whitespace-only value falls back to the documented default. A value that is set but unparseable or out of range is an error — a typo should fail the process at startup, not quietly run at a default nobody asked for.

Consumer sizing

ConsumerTuning covers the three knobs every backend's consumer-group config takes.

VariableTypeDefault
{PREFIX}_MIN_CONSUMERSu16, >= 11
{PREFIX}_MAX_CONSUMERSu16, >= 1same as min (fixed-size group)
{PREFIX}_PREFETCH_COUNTu16, >= 1unset — keep the backend default

Setting only MAX_CONSUMERS gives an autoscaling group starting from 1; setting only MIN_CONSUMERS pins a fixed-size group. MIN > MAX is an error that names both variables.

use shove::RabbitMq;
use shove::consumer_group::ConsumerGroupConfig;
use shove::env::ConsumerTuning;
use shove::rabbitmq::RabbitMqConsumerGroupConfig;
 
let tuning = ConsumerTuning::from_env("ORDERS")?;
 
let config: ConsumerGroupConfig<RabbitMq> = ConsumerGroupConfig::new(
    RabbitMqConsumerGroupConfig::new(tuning.range())
        .with_prefetch_count(tuning.prefetch_count_or(10)),
);
 
group.register::<Orders, _>(config, || OrderHandler).await?;

prefetch_count_or keeps the call site a single unconditional call: it returns the configured value when the variable is set and your own default when it isn't.

Every coordinated-group backend — RabbitMQ, NATS, Kafka, Redis, in-memory — takes the same new(range) + .with_prefetch_count(n) shape, so one tuning feeds all five unchanged. SQS has no consumer group; it uses ConsumerSupervisor, whose register takes ConsumerOptions, so prefetch_count_or still applies there and range() does not.

Autoscaler

VariableTypeDefault
{PREFIX}_POLL_INTERVAL_SECSu64, >= 15
{PREFIX}_SCALE_UP_MULTIPLIERf64, > 02.0
{PREFIX}_SCALE_DOWN_MULTIPLIERf64, > 00.5
{PREFIX}_HYSTERESIS_SECSu6410
{PREFIX}_COOLDOWN_SECSu6430
use shove::autoscaler::AutoscalerConfig;
 
let autoscaler = AutoscalerConfig::from_env("ORDERS")?;

SCALE_DOWN_MULTIPLIER must be strictly below SCALE_UP_MULTIPLIER. At or above it, both scaling conditions can hold at the same queue depth and the group flaps — so that combination is rejected at startup rather than discovered in production. See Performance Tuning for how to pick the multipliers themselves.

NATS JetStream streams

VariableTypeDefault
{PREFIX}_RETENTIONwork_queue | limits | interestwork_queue
{PREFIX}_MAX_AGE_SECSu64, >= 1unlimited
{PREFIX}_MAX_BYTESi64, >= 1unlimited
{PREFIX}_MAX_MESSAGESi64, >= 1unlimited
{PREFIX}_NUM_REPLICASusize, 1..=51
use shove::NatsStreamConfig;
 
let stream = NatsStreamConfig::from_env("EVENTS")?;

RETENTION matching ignores case and treats - and _ alike, so work_queue, work-queue, and WorkQueue all resolve to the same policy. NUM_REPLICAS is capped at 5 because that is JetStream's own maximum. The size bounds reject 0 rather than reading it as JetStream's "unlimited" — an operator writing 0 means the opposite.

Kafka topic durability

VariableTypeDefault
{PREFIX}_REPLICATION_FACTORi32, 1..=32767unset
{PREFIX}_MIN_PARTITIONSi32, >= 1unset

Both stay Option because "unset" has to remain distinguishable from 1: a single-broker dev cluster and a production R3 cluster run the same binary, and shove's own defaults should apply when the operator says nothing.

use shove::env::KafkaTopicTuning;
 
let tuning = KafkaTopicTuning::from_env("INGEST")?;
 
let mut declarer = broker.topology();
if let Some(rf) = tuning.replication_factor() {
    declarer = declarer.with_replication_factor(rf);
}
if let Some(n) = tuning.min_partitions() {
    declarer = declarer.with_min_partitions(n);
}
declarer.declare::<IngestionTopic>().await?;

Knobs shove doesn't model

EnvVars is the primitive the constructors above are built on, and it is public. Use it for your own service's knobs so they parse, range-check, and report errors the same way:

use shove::env::EnvVars;
 
let vars = EnvVars::with_prefix("ORDERS");
 
let batch_size: usize = vars.parse_in("BATCH_SIZE", 100, 1..=10_000)?;
let flush_interval = vars.secs("FLUSH_INTERVAL_SECS", Duration::from_secs(5))?;
let dry_run = vars.flag("DRY_RUN", false)?;

Available readers: get, parse, parse_in, opt_parse, opt_parse_in, flag, secs, opt_secs, choice, and invalid (for cross-field rules the typed readers can't express — it produces an error that names the variable).

Testing your wiring

std::env::set_var is unsafe in edition 2024 and racy across test threads, so every reader has a from_pairs / from_vars twin that reads an explicit map instead of the process environment:

use shove::env::ConsumerTuning;
 
let tuning = ConsumerTuning::from_pairs(
    "ORDERS",
    [("ORDERS_MIN_CONSUMERS", "2"), ("ORDERS_MAX_CONSUMERS", "16")],
)?;
assert_eq!(tuning.range(), 2..=16);

AutoscalerConfig::from_vars, NatsStreamConfig::from_vars, and KafkaTopicTuning::from_vars all take an EnvVars, so one reader can populate several config structs from the same source.