Part 4: Async Programming with Tokio - Concurrent Execution

Introduction

Testing 280 payloads sequentially would take minutes. My WAF scanner needed to test multiple payloads concurrently while respecting rate limits. Rust's async/await with Tokio made this possible—testing dozens of requests simultaneously while maintaining memory safety.

This article shows how I built concurrent scanning into simple-waf-scannerarrow-up-right using async Rust and the Tokio runtime.

Why Async in Rust?

The Problem: Waiting on I/O

// Synchronous code - blocking
fn test_payloads_sync(payloads: &[Payload]) {
    for payload in payloads {
        let response = make_http_request(payload);  // Blocks here
        process_response(response);
    }
}
// 280 payloads × 100ms per request = 28 seconds!

The Solution: Async/Await

// Asynchronous code - concurrent
async fn test_payloads_async(payloads: &[Payload]) {
    let mut tasks = Vec::new();
    
    for payload in payloads {
        let task = tokio::spawn(async move {
            let response = make_http_request(payload).await;  // Doesn't block
            process_response(response).await
        });
        tasks.push(task);
    }
    
    for task in tasks {
        task.await.unwrap();
    }
}
// 280 payloads running concurrently = ~2 seconds!

Setting Up Tokio

Add to Cargo.toml

Async Main Function

Async/Await Basics

Async Functions Return Futures

Futures Are Lazy

Real Example: HTTP Requests

Single Async Request

Concurrent Requests

Controlling Concurrency

Problem: Too Many Concurrent Requests

Solution: Semaphore for Limiting

Better Solution: Stream with Concurrency Limit

Rate Limiting

Adding Delays Between Requests

Token Bucket Rate Limiter

Real Implementation from My Scanner

Complete concurrent scanning with limits:

Async Error Handling

Propagating Errors in Async

Timeout for Async Operations

Channels for Communication

Sending Results Between Tasks

Select - Racing Futures

Join - Waiting for Multiple Futures

Testing Async Code

Key Takeaways

  1. Async/await for I/O: Don't block on network/disk operations

  2. Tokio runtime: Executes async tasks concurrently

  3. Spawning tasks: tokio::spawn for concurrent execution

  4. Limit concurrency: Use semaphores or streams

  5. Rate limiting: Add delays between requests

  6. Channels: Communicate between tasks

  7. Error handling: Async errors work like sync errors

  8. Testing: Use #[tokio::test] for async tests

Common Patterns

Next in Series

In Part 5: HTTP Clients and Real-World Integration, we'll put everything together—building the complete WAF scanner with HTTP requests, JSON handling, CLI parsing, and publishing to crates.io.


Based on implementing concurrent payload testing in simple-waf-scanner, achieving 10-50x speedup over sequential execution.

Last updated