Best practice for async configuration management with RwLock and Serde?

โš“ Rust    ๐Ÿ“… 2025-09-06    ๐Ÿ‘ค surdeus    ๐Ÿ‘๏ธ 1      

surdeus

Iโ€™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 Config struct 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 Config struct 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

Read full topic

๐Ÿท๏ธ Rust_feed