{"record":{"id":"720a6a887578e6fd","repo":"linera-io/linera-protocol","slug":"not-enough-unlocked-balance-try-again-later","errorCode":null,"errorMessage":"Not enough unlocked balance; try again later.","messagePattern":"Not enough unlocked balance; try again later\\.","errorType":"http","errorClass":"async_graphql::Error","httpStatus":null,"severity":"warning","filePath":"linera-faucet/server/src/lib.rs","lineNumber":911,"sourceCode":"            .iter()\n            .fold(Amount::ZERO, |acc, r| acc.saturating_add(r.amount));\n        let Ok(remaining_balance) = balance.try_sub(total_amount) else {\n            // Not enough balance - reject all requests\n            #[cfg(with_metrics)]\n            metrics::INSUFFICIENT_BALANCE_REJECTIONS\n                .with_label_values(&[])\n                .inc();\n            return Err(Error::new(\"The faucet is empty.\"));\n        };\n\n        // Rate limit: Locked token balance decreases lineraly with time, i.e.:\n        // remaining_balance / remaining_duration >= start_balance / full_duration\n        if multiply(u128::from(self.config.start_balance), remaining_duration)\n            > multiply(u128::from(remaining_balance), full_duration)\n        {\n            #[cfg(with_metrics)]\n            metrics::RATE_LIMIT_REJECTIONS.with_label_values(&[]).inc();\n            return Err(Error::new(\"Not enough unlocked balance; try again later.\"));\n        }\n        Ok(())\n    }\n\n    /// Sends an error response to all requestors.\n    fn send_err(requests: Vec<PendingRequest>, err: impl Into<async_graphql::Error>) {\n        let err = err.into();\n        for request in requests {\n            request.send_err(err.clone());\n        }\n    }\n\n    /// Executes a batch of chain creation and/or token transfer requests.\n    async fn execute_batch(&mut self, requests: Vec<PendingRequest>) -> anyhow::Result<()> {\n        if let Err(err) = self.check_rate_limiting(&requests).await {\n            tracing::debug!(\"Rejecting requests due to rate limiting: {err:?}\");\n            Self::send_err(requests, err);\n            return Ok(());","sourceCodeStart":893,"sourceCodeEnd":929,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-faucet/server/src/lib.rs#L893-L929","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry with backoff — the allowed payout grows continuously as time passes.","If you operate the faucet, raise `start_balance` or extend `end_timestamp` so the unlock rate covers the load.","Verify `start_timestamp`/`end_timestamp` configuration; a mis-set window makes the schedule unreachable.","Pace requests client-side instead of submitting bursts."],"exampleFix":"// before\nawait faucet.claim(owner); // throws when the current burst exceeds the unlock rate\n\n// after\nasync function claimWithBackoff(owner) {\n  for (let delay = 30_000; ; delay = Math.min(delay * 2, 1_800_000)) {\n    try { return await faucet.claim(owner); }\n    catch (e) { if (!/not enough unlocked balance/i.test(String(e.message))) throw e; await sleep(delay); }\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"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); } }","preventionTips":["Pace claims client-side instead of bursting; the unlock schedule is linear in time.","Operators: size start_balance and end_timestamp to the expected claim rate.","Check faucet config start/end timestamps after any redeploy."],"tags":["faucet","rate-limit","token-unlock","retry-later"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}