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
- Retry later — faucet operators monitor the `FAUCET_BALANCE` metric and refill the faucet chain.
- If you operate the faucet, transfer more tokens to the faucet chain or lower the configured claim amounts.
- Check the faucet metrics endpoint to distinguish 'empty' (balance too low) from rate limiting.
- 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
- Watch the faucet's FAUCET_BALANCE metric if you operate it; refill before it hits zero.
- Use exponential backoff starting at minutes, not seconds.
- Distinguish 'The faucet is empty.' (funding) from 'Not enough unlocked balance' (schedule) before choosing a retry interval.
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
- No chain created for owner {}
- please specify one of `--faucet` or `--genesis`.
- Failed to start faucet
- no admin chain (Root(0)) in genesis config
- The chain with the ID returned by the faucet is not owned by
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/60c4e997bacf3e30.
Report an issue: GitHub.