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
- Raise the energy budget for the offending call (max_energy in the call/schedule parameters) and retry.
- Optimize the reducer: cap iterations, use indexes, avoid full-table scans and nested loops.
- Split the work: paginate across multiple reducer calls or scheduled increments.
- 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
- Set explicit max_energy on calls that touch large tables instead of relying on defaults.
- Cap loop iterations inside reducers and continue work via scheduled follow-up calls.
- Alert on budget-exceeded outcomes so dataset growth is caught before hard failures.
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
- unsupported type for :sender: {ty:?}
- reducer transaction update {request_id} ran out of energy
- cannot deserialize refs without a typespace
- There is already a reducer, procedure, or view with the name
- Table ${tableName} defines a schedule, but it seems like the
AI-assisted analysis of clockworklabs/SpacetimeDB@fdd647dfac (2026-08-20).
Data as JSON: /api/errors/c37e628f3d087a2d.
Report an issue: GitHub.