clockworklabs/SpacetimeDB · error · anyhow::Error

reducer ran out of energy

Error message

reducer ran out of energy

What it means

Every reducer call is metered with an energy budget; the host aborts the reducer and rolls back its transaction when the budget is spent before the reducer returns. The host models this as ReducerOutcome::BudgetExceeded, and ReducerOutcome::into_result converts that variant into this anyhow error. Seeing it means the reducer was killed for resource exhaustion, not for a logic failure.

Source

Thrown at crates/core/src/host/host_controller.rs:278

impl From<ReducerCallResult> for Result<(), anyhow::Error> {
    fn from(value: ReducerCallResult) -> Self {
        value.outcome.into_result()
    }
}

#[derive(Clone, Debug)]
pub enum ReducerOutcome {
    Committed,
    Failed(Box<Box<str>>),
    BudgetExceeded,
}

impl ReducerOutcome {
    pub fn into_result(self) -> anyhow::Result<()> {
        match self {
            Self::Committed => Ok(()),
            Self::Failed(e) => Err(anyhow::anyhow!(e)),
            Self::BudgetExceeded => Err(anyhow::anyhow!("reducer ran out of energy")),
        }
    }

    pub fn is_err(&self) -> bool {
        !matches!(self, Self::Committed)
    }
}

impl From<&EventStatus> for ReducerOutcome {
    fn from(status: &EventStatus) -> Self {
        match &status {
            EventStatus::Committed(_) => ReducerOutcome::Committed,
            EventStatus::FailedUser(e) | EventStatus::FailedInternal(e) => {
                ReducerOutcome::Failed(Box::new((&**e).into()))
            }
            EventStatus::OutOfEnergy => ReducerOutcome::BudgetExceeded,
        }
    }

View on GitHub (pinned to fdd647dfac)

Solutions

  1. Raise the energy budget for the offending call (max_energy in the call/schedule parameters) and retry.
  2. Optimize the reducer: cap iterations, use indexes, avoid full-table scans and nested loops.
  3. Split the work: paginate across multiple reducer calls or scheduled increments.
  4. Match on ReducerOutcome before calling into_result so budget exhaustion is handled distinctly from Failed(e).

Example fix

// before
let outcome = call_reducer(/* ... */);
outcome.into_result()?; // opaque: reducer ran out of energy

// after
match outcome {
    ReducerOutcome::BudgetExceeded => {
        // shrink the batch or raise the budget, then reschedule
    }
    ReducerOutcome::Failed(e) => return Err(anyhow::anyhow!(e)),
    ReducerOutcome::Committed => {}
}
Defensive patterns

Strategy: type-guard

Type guard

fn is_budget_exceeded(o: &ReducerOutcome) -> bool {
    matches!(o, ReducerOutcome::BudgetExceeded)
}

Try / catch

Match on the ReducerOutcome/EventStatus before calling into_result(); only map Failed(e) through. Treat BudgetExceeded as a signal to raise the budget or shrink the batch and reschedule, not as a generic failure.

Prevention

When it happens

Trigger: A reducer loops over a large table, a long init/migration reducer runs during publish, or a scheduled reducer executes with the default system budget and consumes more energy than the CallReducerParams budget allotted; into_result() then surfaces 'reducer ran out of energy'.

Common situations: Workload grows past the configured budget between deploys; a caller forgets or lowers the max energy setting; an unbounded loop in reducer code; a version upgrade changes energy accounting so previously-fitting reducers now exceed the budget.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@fdd647dfac (2026-08-20). Data as JSON: /api/errors/c37e628f3d087a2d. Report an issue: GitHub.