Blog
async_sync: make sync + async play nice in Rust
async_sync: make sync + async play nice in Rust🔗
TL;DR — async_sync is a tiny crate I wrote to make boundaries between synchronous and asynchronous code painless. It wraps the boring-but-essential stuff—retries (with backoff), timeouts (with optional cleanup), parallel execution of sync tasks, and lightweight diagnostics—so you can focus on your logic instead of glue code. As of today it's passed 1,026 downloads 🎉
Why this exists🔗
Rust gives us excellent async runtimes (Tokio, async-std), but real systems still have plenty of blocking, synchronous bits:
- CPU-heavy transforms
- Non-async libraries (bindings, SDKs)
- Legacy code you can't rewrite right now
You can sprinkle spawn_blocking (Tokio) or thread pools around, but you'll quickly reinvent: retry loops, exponential backoff, timeouts, cleanup hooks, and some kind of "parallel map" for sync functions. async_sync gives you those patterns in one place—consistent and tested—so your app code stays tidy.
Features at a glance🔗
- Retry mechanisms with constant / linear / exponential backoff (add jitter if you like).
- Timeouts with optional cleanup logic when you bail.
- Parallel execution of synchronous tasks (runs them concurrently and collects results).
- Runtime integration for Tokio and async-std—pick your flavor.
- Diagnostics: basic timing/log hooks so you can observe what's happening.
Getting started🔗
# Cargo.toml
[dependencies]
async_sync = "0.1.0"
tokio = { version = "1", features = ["full"] }
# or async-std, if you prefer
Examples🔗
1) Retry with exponential backoff🔗
Handle transient failures in sync code cleanly:
use async_sync::{sync_to_async_with_retries, Backoff};
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn flaky_function(counter: Arc<Mutex<i32>>) -> Result<String, &'static str> {
let mut count = counter.lock().unwrap();
*count += 1;
if *count < 3 { Err("Failure") } else { Ok("Success") }
}
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0));
let result = sync_to_async_with_retries(
|| flaky_function(Arc::clone(&counter)),
5, // max retries
Duration::from_millis(100), // initial delay
Backoff::Exponential, // backoff strategy
).await;
match result {
Ok(v) => println!("Task succeeded: {v}"),
Err(e) => eprintln!("Task failed: {e:?}"),
}
}
Tip: if you want jitter to avoid thundering-herds, add a small random component to the delay between attempts.
2) Parallel execution of sync tasks🔗
Run CPU-bound or blocking tasks in parallel and collect results:
use async_sync::parallel_sync_to_async;
fn heavy_computation(x: i32) -> i32 {
std::thread::sleep(std::time::Duration::from_millis(200));
x * x
}
#[tokio::main]
async fn main() {
let tasks: Vec<_> = (1..=5).map(|x| move || heavy_computation(x)).collect();
let results = parallel_sync_to_async(tasks).await;
for (i, res) in results.into_iter().enumerate() {
println!("Task {} result: {:?}", i + 1, res.unwrap());
}
}
This is perfect for "batch a bunch of sync calls and await the whole group" patterns.
3) Timeouts with cleanup🔗
Ensure a blocking operation doesn't hold your system hostage—and clean up when it does.
use async_sync::timeout_sync;
use std::time::Duration;
fn write_then_flush() -> Result<(), &'static str> {
// ... do work that might stall ...
Ok(())
}
#[tokio::main]
async fn main() {
let res = timeout_sync(
Duration::from_secs(2),
|| write_then_flush(),
Some(|| {
// optional cleanup when timeout triggers
// e.g., close file handles, roll back state, etc.
eprintln!("cleanup after timeout");
})
).await;
match res {
Ok(r) => println!("finished: {:?}", r),
Err(_) => eprintln!("timed out"),
}
}
4) Pick your runtime (Tokio / async-std)🔗
You don't have to rewrite sync functions just to run them under your runtime:
use async_sync::{sync_to_async_with_runtime, Runtime};
fn heavy_computation(x: i32) -> i32 {
std::thread::sleep(std::time::Duration::from_millis(200));
x * x
}
fn main() {
let result = sync_to_async_with_runtime(Runtime::Tokio, || heavy_computation(4));
println!("Result: {:?}", result.unwrap());
}
Swap Runtime::Tokio for Runtime::AsyncStd as needed.
But… why not just use spawn_blocking?🔗
You absolutely can—and under the hood, the approach is similar. The point of async_sync is to package the patterns you'll add every time:
- Retry with configurable backoff (constant/linear/exponential + jitter)
- Timeouts with cleanup hooks
- Parallel orchestration of multiple sync tasks and consistent result handling
- Diagnostics you can turn on to see durations and failures
Instead of hand-rolling those per project, you call one function and focus on business logic.
Diagnostics and observability🔗
There's basic timing and logging so you can trace how long tasks took, which retries fired, and which paths timed out. The goal isn't to be a logging framework—just enough signal to debug and optimize.
Production notes & gotchas🔗
- Don't block the async reactor: everything here routes sync work to blocking threads so your async tasks keep flowing.
- Bound your concurrency: parallel execution is powerful; use bounded pools or back-pressure in higher layers if you're hammering databases or APIs.
- Cleanup hooks matter: when a timeout triggers, use the cleanup closure to reset state or free resources—lots of subtle bugs vanish when you do this consistently.
- Mix and match: it's fine to use
spawn_blockingelsewhere—async_sync is about convenience where you need retries/backoff/timeout/parallel together.
Roadmap / ideas🔗
- Built-in jitter helpers for backoff
- Optional tracing feature-gate for richer spans
- A bounded parallel helper (e.g., "run N-at-a-time" over an iterator)
- Derive-friendly error wrappers for nicer ergonomics on
Result<T, E>
If any of that would help your use case, open an issue!
Links & source🔗
- Crate: https://crates.io/crates/async_sync
- Repository: https://github.com/packetandpine/async_sync
If you kick the tires, I'd love feedback—PRs, issues, and "here's how we used it in production" stories welcome. And thanks to everyone who helped push it past 1,000+ downloads already!
AI-assisted writing
I draft and edit all articles myself, and I use AI as an assistant for outlining, phrasing, and cleanup. Curious how I use it—and where I draw the lines?