Best practice for async configuration management with RwLock and Serde?
โ Rust ๐ 2025-09-06 ๐ค surdeus ๐๏ธ 10Iโve been working on a configuration system for my application and ran into some design issues regarding RwLock + async/await + Serde.
My requirement:
- The application loads a
Configstruct from a file at startup. - It may occasionally be updated by the user (write is rare).
- Most of the time, itโs only read by other async functions.
First attempt: tokio::sync::RwLock
use tokio::sync::RwLock;
pub struct Config {
pub qb: RwLock<QbConfig>,
// ...
}
pub struct QbConfig {
pub host: String,
// ...
}
This works fine for async reads/writes, but itโs hard to serialize/deserialize because tokio::RwLock<T> doesnโt implement Serialize / Deserialize.
Second attempt: std::sync::RwLock
use std::sync::RwLock;
pub struct Config {
pub qb: RwLock<QbConfig>,
// ...
}
pub struct QbConfig {
pub host: String,
// ...
}
This is easier to work with in terms of Serde, but I quickly hit another issue:
std::sync::RwLockReadGuard is not Send, so I canโt hold the guard across .await. For example:
let qb_config: RwLockReadGuard<QbConfig> = qb_config(); // from OnceLock<Config>
async_fn(&qb_config.host).await; // error: non-Send across await
Workaround: use Arc<str>
I changed the field type to Arc<str> so I can cheaply clone it out of the lock before calling async code:
pub struct QbConfig {
pub host: Arc<str>,
// ...
}
And then:
let host = {
let qb_config = qb_config();
qb_config.host.clone()
};
async_fn(host).await;
This works, but it feels clunky.
Question
Are there more idiomatic approaches or best practices for this scenario?
- A
Configstruct loaded once, rarely updated, mostly read. - Needs to be serializable/deserializable.
- Should be easy to use inside async functions without awkward
Arc<str>tricks.
3 posts - 2 participants
๐ท๏ธ Rust_feed