linera-io/linera-protocol · error
Failed to save claims to database
Error message
Failed to save claims to database: {e} What it means
The faucet successfully committed the block that created chains / granted tokens on-chain, but then failed to persist the records to its own database: futures::try_join!(store_chains_batch, store_daily_claims_batch) errored. On-chain state and the faucet's DB are now out of sync — chains exist but are not recorded, which breaks the faucet's rate limiting and idempotency bookkeeping. The error message is sent to all waiting requesters and the batch processing aborts.
Solutions
- Check the {e} inner error in faucet logs — it is the storage backend's message (table missing, auth, throttling).
- Restore DB connectivity/capacity (start local Dynamo, fix credentials/tables, raise write capacity).
- Reconcile: users whose chains were created but not recorded — they will be told the claim failed, and may need the record inserted manually or the daily-limit reset, otherwise re-claiming an existing chain can misbehave.
- Restart the faucet; queued requests re-attempt since failed responses were already sent.
Example fix
// before
if let Err(e) = futures::try_join!(store_initial, store_daily) {
let error_msg = format!("Failed to save claims to database: {e}");
Self::send_err(requests, error_msg.clone());
anyhow::bail!(error_msg);
}
// after: retry transient storage failures before giving up (on-chain state already committed)
let mut attempt = 0;
loop {
let r = futures::try_join!(store_initial(), store_daily());
match r {
Ok(()) => break,
Err(e) if attempt < 3 => { attempt += 1; tokio::time::sleep(Duration::from_secs(2_u64.pow(attempt))).await; }
Err(e) => { let m = format!("Failed to save claims to database: {e}"); Self::send_err(requests, m.clone()); anyhow::bail!(m); }
}
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight storage health before accepting claims: self.faucet_storage.store_daily_claims_batch(vec![]).await?; // or an explicit ping/health call
Try / catch
// On 'Failed to save claims to database': retry with backoff (transient DB outage); // after max attempts, reconcile manually — on-chain chains exist but the DB lacks records, // so re-processing the same requests must be idempotent (key by owner/period).
Prevention
- Provision/verify DB tables and capacity before opening the faucet (migration step in deployment).
- Alert on storage errors so drift is caught before users re-claim created chains.
- Make claim storage idempotent (upsert by owner + daily period) so retries after partial failure are safe.
When it happens
Trigger: process_batch commits the certificate, then store_chains_batch or store_daily_claims_batch hits a storage failure: DynamoDB/table not created, connection dropped, permission denied, write-capacity exceeded, or schema mismatch after a faucet version change.
Common situations: AWS DynamoDB (or configured store) credentials/tables missing or misconfigured; DB briefly unavailable during a batch; faucet upgraded without running the storage migration; local Dynamo not started in dev.
Related errors
- no errors present but no data returned
- Returned AccountInfo should have code: Some(...) and so…
- Returned AccountInfo should have code: Some(...) and so…
- a successful running of the server
- Admin HTTP server exited unexpectedly
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/56a2a2348a099db9.
Report an issue: GitHub.
Appendix: source
Thrown at linera-faucet/server/src/lib.rs:1083
return Ok(());
}
self.faucet_storage
.store_chains_batch(initial_chains, block_timestamp)
.await
};
let store_daily = async {
if daily_claims.is_empty() {
return Ok(());
}
self.faucet_storage
.store_daily_claims_batch(daily_claims)
.await
};
if let Err(e) = futures::try_join!(store_initial, store_daily) {
let error_msg = format!("Failed to save claims to database: {e}");
Self::send_err(requests, error_msg.clone());
anyhow::bail!(error_msg);
}
// Respond to requests.
#[cfg(with_metrics)]
let chains_created = initial_desc_map.len();
for request in requests {
#[cfg(with_metrics)]
{
let wait_time = request.queued_at.elapsed().as_secs_f64() * 1000.0;
metrics::QUEUE_WAIT_TIME
.with_label_values(&[])
.observe(wait_time);
}
let response = if let Some(target_chain_id) = request.target_chain_id {
PendingResponse::Daily(Ok(ClaimOutcome {
chain_id: target_chain_id,View on GitHub (pinned to 6c226ddcb3)