linera-io/linera-protocol · error · async_graphql::Error

The faucet is empty.

Error message

The faucet is empty.

What it means

Raised in the faucet's `check_rate_limiting` step before a batch executes: the sum of the batch's claim amounts exceeds the faucet chain's current local balance (`balance.try_sub(total_amount)` fails), so every request in the batch is rejected with this message (metric `INSUFFICIENT_BALANCE_REJECTIONS`). It indicates the faucet is drained or underfunded relative to the queued batch — a server-side funding problem, not a client mistake.

Source

Thrown at linera-faucet/server/src/lib.rs:901

        let full_duration = end_timestamp.delta_since(start_timestamp).as_micros();
        let remaining_duration = end_timestamp.delta_since(local_time).as_micros();
        let balance = self.client.local_balance().await?;

        #[cfg(with_metrics)]
        metrics::FAUCET_BALANCE
            .with_label_values(&[])
            .set(f64::from(balance));

        let total_amount = requests
            .iter()
            .fold(Amount::ZERO, |acc, r| acc.saturating_add(r.amount));
        let Ok(remaining_balance) = balance.try_sub(total_amount) else {
            // Not enough balance - reject all requests
            #[cfg(with_metrics)]
            metrics::INSUFFICIENT_BALANCE_REJECTIONS
                .with_label_values(&[])
                .inc();
            return Err(Error::new("The faucet is empty."));
        };

        // Rate limit: Locked token balance decreases lineraly with time, i.e.:
        // remaining_balance / remaining_duration >= start_balance / full_duration
        if multiply(u128::from(self.config.start_balance), remaining_duration)
            > multiply(u128::from(remaining_balance), full_duration)
        {
            #[cfg(with_metrics)]
            metrics::RATE_LIMIT_REJECTIONS.with_label_values(&[]).inc();
            return Err(Error::new("Not enough unlocked balance; try again later."));
        }
        Ok(())
    }

    /// Sends an error response to all requestors.
    fn send_err(requests: Vec<PendingRequest>, err: impl Into<async_graphql::Error>) {
        let err = err.into();
        for request in requests {

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Retry later — faucet operators monitor the `FAUCET_BALANCE` metric and refill the faucet chain.
  2. If you operate the faucet, transfer more tokens to the faucet chain or lower the configured claim amounts.
  3. Check the faucet metrics endpoint to distinguish 'empty' (balance too low) from rate limiting.
  4. Reduce concurrent claim bursts so batches are smaller than the remaining balance.

Example fix

// before
while (true) { try { await faucet.claim(owner); break; } catch (e) { /* tight retry loop hammers empty faucet */ } }

// after
for (let delay = 60_000; ; delay = Math.min(delay * 2, 3_600_000)) {
  try { await faucet.claim(owner); break; }
  catch (e) { if (!/faucet is empty/i.test(String(e.message))) throw e; await sleep(delay); }
}
Defensive patterns

Strategy: retry

Try / catch

for (const delay of backoff()) { try { return await faucet.claim(owner); } catch (e) { if (!/faucet is empty/i.test(e.message)) throw e; await sleep(delay); } }

Prevention

When it happens

Trigger: Faucet chain balance drops below the combined amounts of the pending batch (initial plus daily claims); faucet configured with an initial claim amount larger than its remaining funds; a burst of claims draining the faucet between operator refills.

Common situations: Public or devnet faucet exhausted by heavy usage; faucet funded once at genesis and never topped up; misconfigured claim amount versus actual faucet balance.

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/60c4e997bacf3e30. Report an issue: GitHub.