Skip to content

Blocking in Async Linter

AI Agent Context (click to expand)

Purpose: Complete guide to using the blocking-in-async linter for detecting blocking operations inside async Rust functions

Scope: Configuration, usage, refactoring patterns, and best practices for blocking-in-async detection in Rust

Overview: Comprehensive documentation for the blocking-in-async linter that detects blocking operations inside async functions in Rust code. Covers how the linter works using Tree-sitter AST analysis to identify std::fs I/O, std::thread::sleep, and std::net calls within async function bodies. Includes configuration options, CLI and library usage, wrapper function exclusions, common refactoring patterns, and CI/CD integration. This rule is novel and not covered by Clippy, filling a gap in the Rust linting ecosystem. Helps teams prevent thread starvation and deadlocks in async Rust applications by suggesting async-compatible alternatives from the Tokio ecosystem.

Dependencies: tree-sitter (Rust parser), Tokio ecosystem (suggested alternatives)

Exports: Usage documentation, configuration examples, refactoring patterns

Related: cli-reference.md for CLI commands, configuration.md for config format, how-to-ignore-violations.md for ignore patterns

Implementation: Tree-sitter AST-based detection with call path extraction and async wrapper function exclusion

This follows the AI-Optimized Documentation Standard.


Try It Now

pip install thailint
thailint blocking-async src/

Example output:

src/lib.rs:15 - Blocking std::fs operation inside async function: fs::read_to_string
  Suggestion: Use tokio::fs equivalents (e.g., tokio::fs::read_to_string) for async-compatible file I/O operations.

Fix it: Replace blocking standard library calls with their Tokio async equivalents.


Overview

The blocking-in-async linter detects blocking operations inside async functions in Rust code. It identifies three categories of blocking calls -- std::fs I/O, std::thread::sleep, and std::net networking -- that can cause thread starvation and deadlocks when used within async contexts.

The Blocking-in-Async Problem

Async runtimes like Tokio use a small thread pool (typically equal to the number of CPU cores) to multiplex many concurrent tasks. When a task calls a blocking operation, it occupies an entire thread for the duration of the blocking call. Other tasks waiting for that thread cannot make progress.

// Dangerous - blocks the async runtime thread
async fn load_config() -> String {
    // This blocks the entire thread while waiting for disk I/O.
    // No other async tasks can run on this thread until the read completes.
    std::fs::read_to_string("config.toml").unwrap()
}

Thread Starvation and Deadlocks

If enough tasks make blocking calls simultaneously, the runtime exhausts its thread pool. Tasks waiting for threads to become available cannot proceed, and tasks holding threads are blocked on I/O. This creates a deadlock condition where the entire application hangs:

Thread 1: blocked on std::fs::read_to_string(...)
Thread 2: blocked on std::fs::write(...)
Thread 3: blocked on std::thread::sleep(...)
Thread 4: blocked on std::net::TcpStream::connect(...)
Tasks 5-100: waiting for a free thread... forever

A Novel Rule Beyond Clippy

This linter addresses a gap in the Rust linting ecosystem. Clippy does not include a lint for detecting blocking standard library calls inside async functions. While experienced Rust developers learn to avoid this pattern, AI-generated code frequently introduces blocking calls inside async functions because language models do not distinguish between sync and async contexts. This linter catches these mistakes automatically.

Benefits

  • Prevent thread starvation: Identify blocking calls before they reach production
  • Catch AI-generated mistakes: AI code generators frequently mix sync and async APIs
  • Suggest alternatives: Each violation includes a specific Tokio replacement
  • Configurable: Tune detection per sub-rule, ignore test code, exclude wrapper functions

How It Works

Tree-sitter AST Detection

The linter uses Tree-sitter to parse Rust source code into an AST and applies the following analysis:

  1. Locate async functions: Identify function items with the async keyword
  2. Walk call expressions: Find all call_expression nodes within async function bodies
  3. Extract call paths: Read scoped_identifier children to reconstruct fully-qualified or short call paths (e.g., std::fs::read_to_string or fs::read_to_string)
  4. Match blocking patterns: Compare call paths against known blocking API signatures
  5. Check wrapper exclusions: Skip calls wrapped in async-safe functions like spawn_blocking
  6. Report violations: Emit diagnostics with rule ID, message, and suggested fix

Three Sub-Rules

The linter enforces three sub-rules, each targeting a different category of blocking operation:

blocking-async.fs-in-async -- Blocking File I/O

Detects std::fs operations inside async functions. These operations block the thread while waiting for disk I/O.

Detected functions: read_to_string, read, write, create_dir, create_dir_all, remove_file, remove_dir, remove_dir_all, rename, copy, metadata, read_dir, canonicalize, read_link

Violation message: "Blocking std::fs operation inside async function: {context}"

Suggestion: "Use tokio::fs equivalents (e.g., tokio::fs::read_to_string) for async-compatible file I/O operations. Blocking std::fs calls in async functions can cause thread starvation and deadlocks."

blocking-async.sleep-in-async -- Blocking Sleep

Detects std::thread::sleep inside async functions. This blocks the entire thread, preventing other async tasks from executing on it.

Detected patterns: std::thread::sleep, thread::sleep

Violation message: "Blocking std::thread::sleep inside async function: {context}"

Suggestion: "Use tokio::time::sleep instead of std::thread::sleep in async functions. Blocking the thread with std::thread::sleep prevents the async runtime from processing other tasks on the same thread."

blocking-async.net-in-async -- Blocking Networking

Detects std::net types inside async functions. These perform synchronous DNS resolution, connection establishment, and data transfer.

Detected types: TcpStream, TcpListener, UdpSocket

Violation message: "Blocking std::net operation inside async function: {context}"

Suggestion: "Use tokio::net equivalents (e.g., tokio::net::TcpStream) for async-compatible networking. Blocking std::net calls in async functions can cause thread starvation and deadlocks in the async runtime."

Wrapper Function Exclusion

The linter recognizes async-safe wrapper functions that correctly offload blocking work to a thread pool. Calls wrapped in these functions are excluded from detection:

  • asyncify -- Common utility to run blocking code on a thread pool
  • spawn_blocking -- Tokio's built-in mechanism to run blocking code
  • block_in_place -- Tokio's mechanism for blocking within the runtime thread
async fn safe_read() -> String {
    // Not flagged -- spawn_blocking correctly offloads to a thread pool
    tokio::task::spawn_blocking(|| {
        std::fs::read_to_string("config.toml").unwrap()
    }).await.unwrap()
}

Configuration

Basic Configuration

Create or update .thailint.yaml:

blocking-async:
  enabled: true
  allow_in_tests: true
  detect_fs_in_async: true
  detect_sleep_in_async: true
  detect_net_in_async: true
  ignore:
    - "examples/"
    - "benches/"
    - "tests/"

Configuration Options

Option Type Default Description
enabled boolean true Enable/disable blocking-in-async linter
allow_in_tests boolean true Allow blocking calls in #[test] functions and #[cfg(test)] modules
detect_fs_in_async boolean true Detect std::fs operations in async functions
detect_sleep_in_async boolean true Detect std::thread::sleep in async functions
detect_net_in_async boolean true Detect std::net blocking calls in async functions
ignore array ["examples/", "benches/", "tests/"] Paths to exclude from analysis

Selective Sub-Rule Configuration

Disable individual sub-rules when specific blocking patterns are acceptable for a project:

# Example: Allow blocking sleep (e.g., for rate-limiting scripts)
# but still catch fs and net blocking
blocking-async:
  enabled: true
  detect_fs_in_async: true
  detect_sleep_in_async: false
  detect_net_in_async: true

JSON Configuration

{
  "blocking-async": {
    "enabled": true,
    "allow_in_tests": true,
    "detect_fs_in_async": true,
    "detect_sleep_in_async": true,
    "detect_net_in_async": true,
    "ignore": ["examples/", "benches/", "tests/"]
  }
}

Ignoring Violations

See How to Ignore Violations for the complete ignore guide.

Quick examples:

// Line-level ignore
std::fs::read_to_string("config.toml").unwrap() // thailint: ignore[blocking-async.fs-in-async] - One-time config load at startup

// File-level ignore
// thailint: ignore-file[blocking-async]

Usage

CLI Mode

Basic Usage

# Check current directory
thailint blocking-async .

# Check specific directory
thailint blocking-async src/

# Check specific file
thailint blocking-async src/server.rs

# Recursive scan
thailint blocking-async --recursive src/

With Configuration

# Use config file
thailint blocking-async --config .thailint.yaml src/

# Auto-discover config (.thailint.yaml or .thailint.json)
thailint blocking-async src/

Output Formats

# Human-readable text (default)
thailint blocking-async src/

# JSON output for CI/CD
thailint blocking-async --format json src/

# SARIF output for GitHub Code Scanning
thailint blocking-async --format sarif src/ > results.sarif

# Verbose output for debugging
thailint blocking-async --verbose src/

# Parallel processing for large codebases
thailint blocking-async --parallel src/

Library Mode

High-Level API

from src import Linter

# Initialize with config file
linter = Linter(config_file='.thailint.yaml')

# Lint directory with blocking-async rule
violations = linter.lint('src/', rules=['blocking-async'])

# Process violations
if violations:
    for v in violations:
        print(f"{v.file_path}:{v.line_number} - {v.message}")

Direct Linter API

from src.linters.blocking_async import lint

# Lint specific path
violations = lint('src/server.rs')

# With custom configuration
violations = lint(
    'src/',
    config={
        'detect_fs_in_async': True,
        'detect_sleep_in_async': True,
        'detect_net_in_async': False,
    }
)

# Process results
for violation in violations:
    print(f"Line {violation.line_number}: {violation.message}")
    print(f"  Suggestion: {violation.suggestion}")

Docker Mode

# Run with default config
docker run --rm -v $(pwd):/workspace \
  washad/thailint:latest blocking-async /workspace/src/

# With custom config file
docker run --rm \
  -v $(pwd):/workspace \
  -v $(pwd)/.thailint.yaml:/config/.thailint.yaml:ro \
  washad/thailint:latest blocking-async \
  --config /config/.thailint.yaml /workspace/src/

Violation Examples

Example 1: Blocking File I/O in Async (blocking-async.fs-in-async)

Code with violation:

use std::fs;

async fn load_user_data(path: &str) -> Result<String, std::io::Error> {
    // Blocks the async runtime thread while reading from disk
    let contents = fs::read_to_string(path)?;
    Ok(contents)
}

async fn save_report(path: &str, data: &[u8]) -> Result<(), std::io::Error> {
    // Blocks the async runtime thread while writing to disk
    fs::write(path, data)?;
    Ok(())
}

Violation messages:

src/data.rs:5 - Blocking std::fs operation inside async function: fs::read_to_string
  Suggestion: Use tokio::fs equivalents (e.g., tokio::fs::read_to_string) for async-compatible file I/O operations.

src/data.rs:11 - Blocking std::fs operation inside async function: fs::write
  Suggestion: Use tokio::fs equivalents (e.g., tokio::fs::read_to_string) for async-compatible file I/O operations.

Refactored code:

use tokio::fs;

async fn load_user_data(path: &str) -> Result<String, std::io::Error> {
    // Async file read -- yields to the runtime while waiting for disk I/O
    let contents = fs::read_to_string(path).await?;
    Ok(contents)
}

async fn save_report(path: &str, data: &[u8]) -> Result<(), std::io::Error> {
    // Async file write -- yields to the runtime while waiting for disk I/O
    fs::write(path, data).await?;
    Ok(())
}

Example 2: Blocking Sleep in Async (blocking-async.sleep-in-async)

Code with violation:

use std::thread;
use std::time::Duration;

async fn retry_with_backoff<F, T>(mut operation: F) -> T
where
    F: FnMut() -> Result<T, String>,
{
    let mut delay = 1;
    loop {
        match operation() {
            Ok(result) => return result,
            Err(_) => {
                // Blocks the entire thread -- other tasks on this thread cannot run
                thread::sleep(Duration::from_secs(delay));
                delay *= 2;
            }
        }
    }
}

Violation message:

src/retry.rs:14 - Blocking std::thread::sleep inside async function: thread::sleep
  Suggestion: Use tokio::time::sleep instead of std::thread::sleep in async functions.

Refactored code:

use std::time::Duration;
use tokio::time;

async fn retry_with_backoff<F, T>(mut operation: F) -> T
where
    F: FnMut() -> Result<T, String>,
{
    let mut delay = 1;
    loop {
        match operation() {
            Ok(result) => return result,
            Err(_) => {
                // Yields to the runtime -- other tasks can run while waiting
                time::sleep(Duration::from_secs(delay)).await;
                delay *= 2;
            }
        }
    }
}

Example 3: Blocking Network I/O in Async (blocking-async.net-in-async)

Code with violation:

use std::net::TcpStream;
use std::io::{Read, Write};

async fn check_service_health(addr: &str) -> bool {
    // Blocking TCP connection -- blocks the async runtime thread
    match TcpStream::connect(addr) {
        Ok(mut stream) => {
            stream.write_all(b"PING").unwrap();
            let mut buf = [0u8; 4];
            stream.read_exact(&mut buf).unwrap();
            &buf == b"PONG"
        }
        Err(_) => false,
    }
}

Violation message:

src/health.rs:6 - Blocking std::net operation inside async function: TcpStream::connect
  Suggestion: Use tokio::net equivalents (e.g., tokio::net::TcpStream) for async-compatible networking.

Refactored code:

use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

async fn check_service_health(addr: &str) -> bool {
    // Async TCP connection -- yields to the runtime during connection and I/O
    match TcpStream::connect(addr).await {
        Ok(mut stream) => {
            stream.write_all(b"PING").await.unwrap();
            let mut buf = [0u8; 4];
            stream.read_exact(&mut buf).await.unwrap();
            &buf == b"PONG"
        }
        Err(_) => false,
    }
}

Refactoring Patterns

Pattern 1: Replace std::fs with tokio::fs

The most straightforward refactoring -- swap std::fs imports for tokio::fs and add .await to each call.

Before:

use std::fs;

async fn process_config() -> Result<(), Box<dyn std::error::Error>> {
    let config = fs::read_to_string("config.toml")?;
    fs::create_dir_all("output/")?;
    fs::write("output/result.json", processed_data)?;
    Ok(())
}

After:

use tokio::fs;

async fn process_config() -> Result<(), Box<dyn std::error::Error>> {
    let config = fs::read_to_string("config.toml").await?;
    fs::create_dir_all("output/").await?;
    fs::write("output/result.json", processed_data).await?;
    Ok(())
}

Key consideration: tokio::fs mirrors the std::fs API, so most replacements are mechanical.

Pattern 2: Replace std::thread::sleep with tokio::time::sleep

Before:

use std::thread;
use std::time::Duration;

async fn poll_status(url: &str) {
    loop {
        let status = check(url).await;
        if status.is_ready() {
            break;
        }
        thread::sleep(Duration::from_millis(500));
    }
}

After:

use std::time::Duration;
use tokio::time;

async fn poll_status(url: &str) {
    loop {
        let status = check(url).await;
        if status.is_ready() {
            break;
        }
        time::sleep(Duration::from_millis(500)).await;
    }
}

Key consideration: tokio::time::sleep returns a Future that must be .awaited. It yields to the runtime, allowing other tasks to progress during the wait.

Pattern 3: Replace std::net with tokio::net

Before:

use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};

async fn run_proxy(listen_addr: &str, upstream_addr: &str) {
    let listener = TcpListener::bind(listen_addr).unwrap();
    for stream in listener.incoming() {
        let mut client = stream.unwrap();
        let mut upstream = TcpStream::connect(upstream_addr).unwrap();
        let mut buf = vec![0u8; 4096];
        let n = client.read(&mut buf).unwrap();
        upstream.write_all(&buf[..n]).unwrap();
    }
}

After:

use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

async fn run_proxy(listen_addr: &str, upstream_addr: &str) {
    let listener = TcpListener::bind(listen_addr).await.unwrap();
    loop {
        let (mut client, _) = listener.accept().await.unwrap();
        let mut upstream = TcpStream::connect(upstream_addr).await.unwrap();
        let mut buf = vec![0u8; 4096];
        let n = client.read(&mut buf).await.unwrap();
        upstream.write_all(&buf[..n]).await.unwrap();
    }
}

Key consideration: tokio::net::TcpListener uses .accept().await instead of .incoming(). Error handling patterns may differ slightly.

Pattern 4: Use spawn_blocking for Unavoidable Blocking Work

When async alternatives do not exist (e.g., CPU-heavy computation, FFI calls, or libraries that only provide synchronous APIs), use spawn_blocking to offload work to a dedicated thread pool.

Before:

async fn compress_file(path: &str) -> Vec<u8> {
    // No async compression library available
    let data = std::fs::read(path).unwrap();
    heavy_compress(&data)  // CPU-bound, blocks the thread
}

After:

async fn compress_file(path: &str) -> Vec<u8> {
    let path = path.to_string();
    tokio::task::spawn_blocking(move || {
        let data = std::fs::read(&path).unwrap();
        heavy_compress(&data)
    }).await.unwrap()
}

Key consideration: spawn_blocking runs the closure on a separate thread pool sized for blocking work. The async task .awaits the result without blocking the runtime.

Pattern 5: Use Async Channels for Producer-Consumer Patterns

When blocking I/O feeds data to async processing, use async channels to decouple the blocking producer from the async consumer.

Before:

async fn watch_directory(path: &str) {
    // Blocking directory scan inside async function
    loop {
        let entries = std::fs::read_dir(path).unwrap();
        for entry in entries {
            let entry = entry.unwrap();
            process_file(entry.path()).await;
        }
        std::thread::sleep(std::time::Duration::from_secs(5));
    }
}

After:

use tokio::sync::mpsc;
use std::path::PathBuf;

async fn watch_directory(path: &str) {
    let (tx, mut rx) = mpsc::channel::<PathBuf>(100);
    let path = path.to_string();

    // Blocking work runs on a dedicated thread
    tokio::task::spawn_blocking(move || {
        loop {
            let entries = std::fs::read_dir(&path).unwrap();
            for entry in entries {
                let entry = entry.unwrap();
                tx.blocking_send(entry.path()).unwrap();
            }
            std::thread::sleep(std::time::Duration::from_secs(5));
        }
    });

    // Async consumer processes files without blocking
    while let Some(file_path) = rx.recv().await {
        process_file(file_path).await;
    }
}

Key consideration: mpsc::Sender::blocking_send allows the blocking thread to send into the channel without requiring an async runtime. The async consumer receives items with .recv().await.

Pattern 6: Use tokio::time::interval for Periodic Tasks

Replace loop + thread::sleep patterns with tokio::time::interval for periodic async work.

Before:

async fn metrics_reporter() {
    loop {
        collect_and_send_metrics().await;
        std::thread::sleep(std::time::Duration::from_secs(60));
    }
}

After:

use tokio::time::{self, Duration};

async fn metrics_reporter() {
    let mut interval = time::interval(Duration::from_secs(60));
    loop {
        interval.tick().await;
        collect_and_send_metrics().await;
    }
}

Key consideration: tokio::time::interval accounts for the time spent in the loop body, providing more consistent timing than sleep at the end of each iteration.

Language Support

Rust Support

Fully Supported

The blocking-in-async linter supports Rust exclusively. It uses Tree-sitter to parse Rust source files and analyze async function bodies for blocking call patterns.

Detection covers:

Category Blocking API Async Alternative
File I/O std::fs::read_to_string tokio::fs::read_to_string
File I/O std::fs::read tokio::fs::read
File I/O std::fs::write tokio::fs::write
File I/O std::fs::create_dir tokio::fs::create_dir
File I/O std::fs::create_dir_all tokio::fs::create_dir_all
File I/O std::fs::remove_file tokio::fs::remove_file
File I/O std::fs::remove_dir tokio::fs::remove_dir
File I/O std::fs::remove_dir_all tokio::fs::remove_dir_all
File I/O std::fs::rename tokio::fs::rename
File I/O std::fs::copy tokio::fs::copy
File I/O std::fs::metadata tokio::fs::metadata
File I/O std::fs::read_dir tokio::fs::read_dir
File I/O std::fs::canonicalize tokio::fs::canonicalize
File I/O std::fs::read_link tokio::fs::read_link
Sleep std::thread::sleep tokio::time::sleep
Networking std::net::TcpStream tokio::net::TcpStream
Networking std::net::TcpListener tokio::net::TcpListener
Networking std::net::UdpSocket tokio::net::UdpSocket

Path matching: Both fully-qualified paths (std::fs::read_to_string) and short paths (fs::read_to_string) are detected.

Other Languages

Blocking-in-async detection is Rust-specific. Other languages handle async/blocking interactions differently:

  • Python: asyncio raises warnings for blocking calls in debug mode
  • TypeScript/JavaScript: Single-threaded event loop; blocking the thread is obvious
  • Go: Goroutines are cheap and blocking is the expected pattern

CI/CD Integration

GitHub Actions

name: Lint

on: [push, pull_request]

jobs:
  blocking-async-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install thailint
        run: pip install thailint

      - name: Check for blocking operations in async functions
        run: |
          thailint blocking-async src/

      - name: SARIF upload (optional)
        if: always()
        run: |
          thailint blocking-async --format sarif src/ > blocking-async.sarif

      - name: Upload SARIF to GitHub
        if: always()
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: blocking-async.sarif

Pre-commit Hook

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: blocking-async-check
        name: Check for blocking operations in async Rust functions
        entry: thailint blocking-async
        language: python
        types: [rust]
        pass_filenames: true

Makefile Integration

lint-blocking-async:
    @echo "=== Checking for blocking operations in async functions ==="
    @poetry run thailint blocking-async src/ || exit 1

lint-all: lint-blocking-async
    @echo "All checks passed"

Performance

The blocking-in-async linter is designed for speed:

Operation Performance Target
Single file parse (Tree-sitter) ~5-20ms <50ms
Single file analysis ~3-10ms <30ms
100 files ~300ms <2s
1000 files ~1-2s <10s

Optimizations:

  • Tree-sitter provides fast incremental parsing of Rust source files
  • AST traversal targets only async function bodies, skipping synchronous code
  • Call path extraction uses efficient pattern matching on scoped identifiers
  • Wrapper function exclusion short-circuits analysis for correctly-wrapped calls
  • Parallel mode (--parallel) distributes file analysis across CPU cores

Troubleshooting

Common Issues

Issue: Blocking call in spawn_blocking is still flagged

// Problem -- nested closure not recognized
async fn read_config() {
    let handle = tokio::task::spawn_blocking(|| {
        let inner = || {
            std::fs::read_to_string("config.toml") // May be flagged
        };
        inner()
    });
    handle.await.unwrap();
}
// Solution -- keep blocking calls directly inside spawn_blocking
async fn read_config() {
    let handle = tokio::task::spawn_blocking(|| {
        std::fs::read_to_string("config.toml").unwrap()
    });
    handle.await.unwrap();
}

Issue: False positive on sync function called from async context

// Problem -- sync helper function uses std::fs
fn read_sync(path: &str) -> String {
    std::fs::read_to_string(path).unwrap() // Not flagged (sync function)
}

async fn handler() {
    let data = read_sync("file.txt"); // The linter flags calls INSIDE async functions
}

The linter only inspects calls directly inside async function bodies. If a synchronous helper uses blocking calls, the linter does not trace through the call graph. Use spawn_blocking to wrap calls to synchronous helpers:

async fn handler() {
    let data = tokio::task::spawn_blocking(|| read_sync("file.txt"))
        .await
        .unwrap();
}

Issue: Test functions are flagged

// Problem -- allow_in_tests may not cover all test patterns
#[tokio::test]
async fn test_file_processing() {
    std::fs::write("test.txt", "data").unwrap(); // Flagged if not recognized as test
}
# Solution -- verify allow_in_tests is enabled and test paths are in ignore list
blocking-async:
  allow_in_tests: true
  ignore:
    - "tests/"
    - "test_*.rs"

Issue: Custom async wrapper not recognized

// Problem -- project uses a custom wrapper function
async fn handler() {
    my_spawn_blocking(|| {
        std::fs::read_to_string("file.txt").unwrap()
    }).await;
}

The linter recognizes asyncify, spawn_blocking, and block_in_place. For custom wrappers, use a line-level ignore:

async fn handler() {
    my_spawn_blocking(|| {
        std::fs::read_to_string("file.txt").unwrap() // thailint: ignore[blocking-async.fs-in-async] - Wrapped in custom spawn_blocking
    }).await;
}

Best Practices

1. Default to Async Alternatives

When writing async Rust code, use Tokio equivalents by default. Treat blocking standard library calls as an explicit choice that requires justification.

// Prefer tokio::fs over std::fs in async code
use tokio::fs;

async fn read_config(path: &str) -> Result<String, std::io::Error> {
    fs::read_to_string(path).await
}

2. Use spawn_blocking for Unavoidable Blocking

When an async alternative does not exist (FFI, CPU-bound work, legacy libraries), explicitly offload to the blocking thread pool.

async fn compute_hash(data: Vec<u8>) -> Vec<u8> {
    tokio::task::spawn_blocking(move || {
        expensive_hash_function(&data)
    }).await.unwrap()
}

3. Keep Blocking and Async Code Separated

Organize code so that synchronous/blocking logic lives in separate modules from async logic. This makes it easier to identify and manage blocking boundaries.

// sync_io.rs -- all blocking I/O in one place
pub fn read_config_sync(path: &str) -> Config {
    let data = std::fs::read_to_string(path).unwrap();
    toml::from_str(&data).unwrap()
}

// async_handlers.rs -- async code delegates to spawn_blocking
pub async fn load_config(path: &str) -> Config {
    let path = path.to_string();
    tokio::task::spawn_blocking(move || {
        sync_io::read_config_sync(&path)
    }).await.unwrap()
}

4. Audit AI-Generated Async Code

AI code generators frequently produce async functions that call blocking APIs. Run the blocking-in-async linter as part of the code review process for AI-generated Rust code.

# Check AI-generated code before merging
thailint blocking-async --format json generated_code/ > report.json

5. Use tokio::time::interval Over loop + sleep

For periodic tasks, prefer tokio::time::interval over manual loop-and-sleep patterns. It provides more consistent timing and integrates with the Tokio runtime.

use tokio::time::{self, Duration};

async fn heartbeat() {
    let mut interval = time::interval(Duration::from_secs(30));
    loop {
        interval.tick().await;
        send_heartbeat().await;
    }
}

6. Enable All Sub-Rules in CI/CD

Run all three sub-rules (fs-in-async, sleep-in-async, net-in-async) in CI/CD pipelines to catch all categories of blocking operations. Disable individual sub-rules only at the project level in .thailint.yaml when a specific pattern is intentional.

# Recommended CI/CD configuration -- all sub-rules enabled
blocking-async:
  enabled: true
  detect_fs_in_async: true
  detect_sleep_in_async: true
  detect_net_in_async: true