Tutorial5 min read

Web Scraping in Rust

Short answer

Web scraping in Rust takes two crates: reqwest to fetch pages (async on Tokio, or its blocking client) and scraper to parse the HTML with CSS selectors. For pages that need JavaScript, thirtyfour or fantoccini drive a browser through WebDriver, and chromiumoxide or headless_chrome speak the Chrome DevTools protocol; Rust pays off when one machine must fetch and parse many pages with little memory. ProxyField residential proxies plug into reqwest with one Proxy::all call from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.

residential IPs
412M+
from, traffic never expires
$1.95/GB
Easy Trial
1 GB for $2.95
On this page 8 sections
  1. The Rust scraping crates
  2. Set up the project
  3. Fetch and parse a page
  4. Many pages at once
  5. Add a proxy
  6. JavaScript-rendered pages
  7. Rust vs Python for web scraping
  8. Web Scraping in Rust FAQ

The Rust scraping crates

CrateWhat it doesVersion (crates.io, 2026-10-23)
reqwestHTTP client: async and blocking, cookies, proxies, gzip0.13.5
scraperParses HTML and selects elements with CSS selectors0.29.0
tokioAsync runtime that reqwest's async client runs on1.54.1
thirtyfourSelenium/WebDriver client0.38.1
fantocciniWebDriver client0.22.1
chromiumoxideChrome DevTools protocol client0.9.1
headless_chromeChrome DevTools protocol client1.0.22
selectOlder HTML extraction crate0.6.1
spiderCrawler and scraper framework2.53.9

For a static page, reqwest plus scraper is enough; add futures when you fetch many pages at once.

Set up the project

Shell
cargo new scraper-demo && cd scraper-demo
cargo add reqwest scraper futures
cargo add tokio --features full

Fetch and parse a page

The example reads the practice sandbox scrapethissite.com, where each of 250 countries sits in a div.country with its name in h3.country-name and its capital in span.country-capital.

Rust
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder()
        .user_agent("Mozilla/5.0 (compatible; rust-scraper/1.0)")
        .timeout(std::time::Duration::from_secs(30))
        .build()?;

    let body = client
        .get("https://www.scrapethissite.com/pages/simple/")
        .send().await?
        .error_for_status()?
        .text().await?;

    for (name, capital) in parse_countries(&body) {
        println!("{name}\t{capital}");
    }
    Ok(())
}

fn parse_countries(html: &str) -> Vec<(String, String)> {
    let doc = Html::parse_document(html);
    let card = Selector::parse("div.country").unwrap();
    let name = Selector::parse("h3.country-name").unwrap();
    let capital = Selector::parse("span.country-capital").unwrap();
    doc.select(&card)
        .map(|c| {
            let text = |sel: &Selector| {
                c.select(sel).next()
                    .map(|e| e.text().collect::<String>().trim().to_string())
                    .unwrap_or_default()
            };
            (text(&name), text(&capital))
        })
        .collect()
}

Three things to know about scraper:

  • Selector::parse returns a Result; build selectors once, outside loops.
  • element.text() yields the text nodes; collect them into a String and trim.
  • Read attributes with element.value().attr("href"), which returns Option<&str>.

Parsing sits in its own function because scraper::Html is not Send: keeping it out of async code that crosses an .await avoids compiler errors when you later spawn tasks.

Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.

Many pages at once

Rust's advantage shows when you fetch many pages concurrently. buffer_unordered(n) keeps n requests in flight; set n with the target site in mind, not your CPU.

Rust
use futures::{stream, StreamExt};

async fn fetch_all(client: &reqwest::Client, urls: Vec<String>) -> Vec<(String, usize)> {
    stream::iter(urls)
        .map(|url| async move {
            let res = client.get(&url).send().await.and_then(|r| r.error_for_status());
            let len = match res {
                Ok(r) => r.text().await.map(|t| t.len()).unwrap_or(0),
                Err(e) => { eprintln!("{url}: {e}"); 0 }
            };
            (url, len)
        })
        .buffer_unordered(8)        // 8 concurrent requests
        .collect()
        .await
}

For pagination on the sandbox's hockey table, build the URLs https://www.scrapethissite.com/pages/forms/?page_num=1 to N and pass them in. Retries are up to you: wrap the request in a loop that retries timeouts and 429 or 5xx responses with a growing delay. For pacing and concurrency in general, see how to make web scraping faster.

Add a proxy

reqwest takes a proxy on the client builder. Proxy::all sends both HTTP and HTTPS requests through it, and basic_auth adds the credentials. The ProxyField gateway is gate.proxyfield.io:7000; <login>-country-us-city-new_york asks for an exit IP in New York. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional.

Rust
let proxy = reqwest::Proxy::all("http://gate.proxyfield.io:7000")?
    .basic_auth("<login>-country-us-city-new_york", "<password>");

let client = reqwest::Client::builder()
    .proxy(proxy)
    .timeout(std::time::Duration::from_secs(30))
    .build()?;

println!("{}", client.get("https://api.ipify.org").send().await?.text().await?);

The gateway can give every request a new IP, or keep one IP for a sticky session from 30 s to 36 h. reqwest also reads the HTTP_PROXY and HTTPS_PROXY environment variables unless you call .no_proxy() on the builder, so check your environment if requests go somewhere you did not expect. Which IP type to use: proxies for web scraping.

JavaScript-rendered pages

scraper parses what the server sends and runs no JavaScript. If the data is missing from the HTML, first look for the JSON request the page makes (DevTools > Network > Fetch/XHR) and call it with reqwest and serde_json. If you need a real browser:

thirtyfour (WebDriver): start chromedriver --port=9515, then:

Rust
use thirtyfour::prelude::*;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let mut caps = DesiredCapabilities::chrome();
    caps.add_arg("--headless=new")?;
    caps.add_arg("--proxy-server=http://gate.proxyfield.io:<port>")?;   // IP allowlist auth
    let driver = WebDriver::new("http://localhost:9515", caps).await?;
    driver.goto("https://example.com/js-page").await?;
    let heading = driver.find(By::Css("h1")).await?;
    println!("{}", heading.text().await?);
    driver.quit().await?;
    Ok(())
}

Chrome's --proxy-server flag carries no username or password, so this setup uses an IP allowlist: add your server's IP to your account's allowlist, and replace <port> with the port you set up in the dashboard for it. thirtyfour is the crate behind most "Rust Selenium" examples; it speaks the same WebDriver protocol as Selenium. The browser side, for any language: how to scrape dynamic web pages.

chromiumoxide or headless_chrome control Chrome directly over the DevTools protocol, without a driver process, and can intercept network requests; they suit jobs that render many pages from one long-running browser.

Rust vs Python for web scraping

FeatureRust (reqwest + scraper)Python (Requests + Beautiful Soup)
Speed of parsingFast, compiledSlower; lxml helps
Memory per concurrent requestLowHigher
ConcurrencyAsync with Tokio, no GILasyncio or threads; GIL for CPU work
Time to first working scriptLonger (types, borrow checker)Short
EcosystemSmaller: few frameworks, fewer examplesLarge: Scrapy, many parsers and tutorials
Browser automationWebDriver and DevTools cratesPlaywright, Selenium, mature

Pick Rust when the scraper runs for a long time at high volume on limited hardware, or when it belongs inside a Rust service. Pick Python when the site changes often and you rewrite selectors weekly: the network is the slow part of most scrapers, so the language rarely decides speed. Language-by-language comparison: choosing a language for web scraping. Other guides in this series: Ruby web scraping and how to build a web crawler.

High-volume scraping in Rust? ProxyField residential proxies: 412M+ residential IPs in 195+ countries, unlimited concurrent threads, an average response of about 400 ms according to our service data. Pay per GB, no subscription; start with the Easy Trial (1 GB). Traffic never expires, and all purchases are final.

Web Scraping in Rust FAQ

How do I scrape a website with Rust?

ProxyField residential proxies, from $1.95/GB, plug into a Rust scraper with one Proxy::all call, and the scraper itself needs three crates: add reqwest, scraper and tokio, fetch the page with reqwest::Client, parse it with Html::parse_document, and select elements with Selector::parse("div.item"), reading text() or value().attr("href"). Fetch many pages with buffer_unordered, and route the client through the rotating residential proxies once one IP is not enough.

Why use Rust for web scraping?

For throughput on modest hardware: compiled code parses fast, async Tokio keeps thousands of requests in flight with little memory, and the type system catches many errors before a long run starts. It is also the natural choice when the scraper lives inside a Rust service.

How does Rust web scraping compare to Python?

Rust is faster at parsing and uses less memory per request, while Python is quicker to write and has a far larger ecosystem, with Scrapy, Playwright and many parsers. Since most scrapers wait on the network, Python is often fast enough, and Rust wins on very large, long-running jobs.

What is the best HTTP client for web scraping in Rust?

reqwest is the usual choice: async and blocking APIs, cookies, redirects, compression and proxy support with authentication. For lower-level control, hyper sits underneath it; ureq is a small blocking client for simple tools.

What is the best HTML parser for Rust?

The scraper crate, which builds on the html5ever parser from the Servo project and supports CSS selectors. select is an older alternative, and lol_html rewrites or extracts from HTML as a stream when pages are very large.

Can Rust scrape JavaScript-rendered websites?

Yes, with a browser: thirtyfour and fantoccini drive Chrome or Firefox through WebDriver, and chromiumoxide and headless_chrome control Chrome over the DevTools protocol. Check first whether the page loads its data from a JSON API you can call with reqwest.

Can we use Selenium in Rust?

Not the official bindings, but the thirtyfour crate is a WebDriver client that works like Selenium: start chromedriver or geckodriver, connect with WebDriver::new, then navigate, find elements and read text. It can also connect to a Selenium Grid.

Is there a ready-made Rust web scraper or crawler crate?

Yes: spider is a crawler and scraper framework with concurrency, and many projects build their own from reqwest, scraper and a queue. For one-off jobs, a short reqwest and scraper program is usually simpler than a framework.

How do I add a proxy to a Rust scraper?

Create reqwest::Proxy::all("http://host:port")?.basic_auth("user", "pass") and pass it to Client::builder().proxy(...). reqwest also picks up HTTP_PROXY and HTTPS_PROXY from the environment unless you call .no_proxy().

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.