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

Not enough unlocked balance; try again later.

Error message

Not enough unlocked balance; try again later.

What it means

The faucet rate-limits payouts so its balance decays linearly over the configured lifetime: a batch is refused when `remaining_balance / remaining_duration < start_balance / full_duration` (metric `RATE_LIMIT_REJECTIONS`). This message means the faucet still holds tokens but they are effectively locked by the schedule — claims are arriving faster than the configured unlock rate between `start_timestamp` and `end_timestamp`. It is transient by design; the whole batch is rejected and retried later.

Source

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

            .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 {
            request.send_err(err.clone());
        }
    }

    /// Executes a batch of chain creation and/or token transfer requests.
    async fn execute_batch(&mut self, requests: Vec<PendingRequest>) -> anyhow::Result<()> {
        if let Err(err) = self.check_rate_limiting(&requests).await {
            tracing::debug!("Rejecting requests due to rate limiting: {err:?}");
            Self::send_err(requests, err);
            return Ok(());

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Retry with backoff — the allowed payout grows continuously as time passes.
  2. If you operate the faucet, raise `start_balance` or extend `end_timestamp` so the unlock rate covers the load.
  3. Verify `start_timestamp`/`end_timestamp` configuration; a mis-set window makes the schedule unreachable.
  4. Pace requests client-side instead of submitting bursts.

Example fix

// before
await faucet.claim(owner); // throws when the current burst exceeds the unlock rate

// after
async function claimWithBackoff(owner) {
  for (let delay = 30_000; ; delay = Math.min(delay * 2, 1_800_000)) {
    try { return await faucet.claim(owner); }
    catch (e) { if (!/not enough unlocked balance/i.test(String(e.message))) throw e; await sleep(delay); }
  }
}
Defensive patterns

Strategy: retry

Try / catch

for (const delay of backoff(30_000, 1_800_000)) { try { return await faucet.claim(owner); } catch (e) { if (!/not enough unlocked balance/i.test(e.message)) throw e; await sleep(delay); } }

Prevention

When it happens

Trigger: Claim rate exceeding the configured linear schedule (burst of claims early in the faucet's lifetime); a batch whose total amount would push the balance below the schedule line; approaching `end_timestamp` with substantial balance still locked.

Common situations: Load tests hammering a faucet configured for a long duration; misconfigured `start_timestamp` (e.g. set in the past, shrinking `full_duration` and inflating the required rate); many users claiming right after a devnet launch.

Related errors


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