     [Blog](https://scrapfly.io/blog)   /  [rust](https://scrapfly.io/blog/tag/rust)   /  [6 Top Rust Web Scraping Libraries and APIs in 2026](https://scrapfly.io/blog/posts/best-rust-web-scraping-libraries)   # 6 Top Rust Web Scraping Libraries and APIs in 2026

 by [Mayada Shaaban](https://scrapfly.io/blog/author/mayada-shaaban-90143e67) Oct 16, 2026 12 min read [\#rust](https://scrapfly.io/blog/tag/rust) [\#tools](https://scrapfly.io/blog/tag/tools) [\#web-scraping](https://scrapfly.io/blog/tag/web-scraping) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries&text=6%20Top%20Rust%20Web%20Scraping%20Libraries%20and%20APIs%20in%202026 "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries "Share on Facebook")    

 

 

Summarize this article with

 [  ](https://chat.openai.com/?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries) [  ](https://claude.ai/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries) [  ](https://x.com/i/grok?text=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries) [  ](https://www.perplexity.ai/search/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries) [  ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fbest-rust-web-scraping-libraries) 



         

Let's collect the product names and prices from `web-scraping.dev/products` with Rust. Four of the six tools below print `Box of Chocolate Candy` as the first product. Those are Scrapfly's Rust SDK, scraper, [thirtyfour](https://docs.rs/thirtyfour/0.37.5/thirtyfour/) and [chromiumoxide](https://docs.rs/chromiumoxide/0.9.1/chromiumoxide/).

The other two do something else with the page: reqwest only downloads it, and [dom\_smoothie](https://docs.rs/dom_smoothie/0.18.2/dom_smoothie/) turns it into readable text.

[What is a Headless Browser? Top 5 Headless Browser ToolsQuick overview of new emerging tech of browser automation - what exactly are these tools and how are they used in web scraping?](https://scrapfly.io/blog/posts/what-is-a-headless-browser)



## Which Rust Web Scraping Library or API Is Best in 2026?

Well, that mostly comes down to what web-scraping.dev sends back to us. The `/products` page has its products right in the html, so a plain download works, but `/reviews` loads its reviews with javascript and we'd need a browser for it. Then there's `/blocked`, which has no data in it at all, no matter how we parse it.

We'll use [Tokio](https://github.com/tokio-rs/tokio) for async in every example below, which is why each one starts with `#[tokio::main]`. And if Rust isn't a must for you, our overview of [web scraping APIs and tools](https://scrapfly.io/blog/posts/best-web-scraping-apis) covers other options too.

So what does a block look like?



## 1. Scrapfly Web Scraping API

Here's what a block looks like from Rust. web-scraping.dev has a `/blocked` page, so let's request it with reqwest and count the products like we would on `/products`:

rust

```rust
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://web-scraping.dev/blocked").await?;
    let status = response.status();
    let html = response.text().await?;
    let document = Html::parse_document(&html);
    let products = document.select(&Selector::parse(".product")?).count();
    let title: String = document.select(&Selector::parse("title")?).flat_map(|n| n.text()).collect();
    println!("status={status} products={products} title={title:?}");
    Ok(())
}
```



text

```text
status=200 OK products=0 title="web-scraping.dev - You've been blocked"
```



The status is 200, so `error_for_status()` wouldn't catch it either. The [Scrapfly Rust SDK](https://scrapfly.io/docs/sdk/rust) sends the request through Scrapfly's API instead, and we parse the html it returns with scraper again.

Let's add it to Cargo.toml next to scraper and tokio, which needs the `macros` and `rt-multi-thread` features for `#[tokio::main]`:

toml

```toml
[dependencies]
reqwest = "=0.13.5"
scrapfly-sdk = "=0.5.1"
scraper = "=0.27.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```



Now we can request the products page through the [Scrapfly Web Scraping API](https://scrapfly.io/products/web-scraping-api):

rust

```rust
use scraper::{Html, Selector};
use scrapfly_sdk::{Client, ScrapeConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder().api_key(std::env::var("SCRAPFLY_KEY")?).build()?;
    let config = ScrapeConfig::builder("https://web-scraping.dev/products").unblocker(true).build()?;
    let result = client.scrape(&config).await?;

    let html = Html::parse_document(&result.result.content);
    for name in html.select(&Selector::parse(".product h3 a")?) {
        println!("{}", name.text().collect::<String>());
    }
    Ok(())
}
```



The key comes from the `SCRAPFLY_KEY` environment variable, and `unblocker(true)` is the part that deals with blocks like the one above. Running it prints:

text

```text
Box of Chocolate Candy
Dark Red Energy Potion
Teal Energy Potion
Red Energy Potion
Blue Energy Potion
```



What each request costs is on the [Scrapfly pricing page](https://scrapfly.io/pricing).

But this product listing doesn't block anyone, so do we even need an API for it? Let's find out with plain HTTP using reqwest.



## 2. reqwest

[reqwest](https://docs.rs/reqwest/0.13.5/reqwest/) is a plain HTTP client, a bit like `requests` in Python. It sends our request and gives us back the html exactly how the server returned it - there's no browser here, so the page's javascript never runs.

Let's see how that works on two pages. We'll add `reqwest = "=0.13.5"` and tokio to Cargo.toml, download each page and check if it contains a piece of text we can see in the browser:

rust

```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pages = [
        ("https://web-scraping.dev/products", "Box of Chocolate Candy"),
        ("https://web-scraping.dev/reviews", "Unique flavor and great energy boost"),
    ];
    for (url, text) in pages {
        let html = reqwest::get(url).await?.error_for_status()?.text().await?;
        println!("{url} bytes={} found={}", html.len(), html.contains(text));
    }
    Ok(())
}
```



The text for the products page is the first product name, and for the [reviews page](https://web-scraping.dev/reviews) it's the newest review. If we run it we'll get:

text

```text
https://web-scraping.dev/products bytes=17153 found=true
https://web-scraping.dev/reviews bytes=14727 found=false
```



The products html has the product name, but the reviews html doesn't have the review at all! That's because the reviews are loaded by javascript after the page opens, and reqwest never runs it.

Still, for the products page the html is all we need. So how do we pull the products out of it?



## 3. scraper

That's what [scraper](https://docs.rs/scraper/0.27.0/scraper/) is for - it's an HTML parsing library which turns an html string into a tree that we can search with CSS selectors, a bit like BeautifulSoup does in Python. Let's add `scraper = "=0.27.0"` to Cargo.toml next to reqwest and tokio and use the two together:

rust

```rust
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .build()?;
    let html = client
        .get("https://web-scraping.dev/products")
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&html);
    let products = Selector::parse(".product")?;
    let name = Selector::parse("h3 a")?;
    let price = Selector::parse(".price")?;
    let rows: Vec<(String, String)> = document
        .select(&products)
        .map(|product| {
            let text = |selector: &Selector| {
                let value: String = product.select(selector).flat_map(|node| node.text()).collect();
                value.trim().to_string()
            };
            (text(&name), text(&price))
        })
        .collect();

    for (title, cost) in &rows {
        println!("{title}: ${cost}");
    }

    println!("products={}", rows.len());
    Ok(())
}
```



Above, we download the products page with reqwest and parse it with scraper:

- `reqwest::Client` is built once with a 30-second timeout, as the reqwest docs recommend reusing a single client.
- `Html::parse_document()` turns the html into a tree we can search.
- `.select()` finds every `.product` card, and the `text` closure pulls the name and price out of each one.

Running it prints the same five products we got through Scrapfly:

text

```text
Box of Chocolate Candy: $24.99
Dark Red Energy Potion: $4.99
Teal Energy Potion: $4.99
Red Energy Potion: $4.99
Blue Energy Potion: $4.99
products=5
```



Since this page isn't protected, plain reqwest is enough here. (For more on the selectors themselves, see our guide to [parsing HTML with CSS selectors](https://scrapfly.io/blog/posts/parsing-html-with-css).)

What if we want to parse the page once and hand the `Html` to another tokio task? Let's try it:

rust

```rust
use scraper::Html;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let html = reqwest::get("https://web-scraping.dev/products").await?.text().await?;
    let document = Html::parse_document(&html);
    tokio::spawn(async move {
        println!("{}", document.root_element().html().len());
    })
    .await?;
    Ok(())
}
```



`cargo build` won't even compile it:

text

```text
error: future cannot be sent between threads safely
  = help: within `tendril::tendril::NonAtomic`, the trait `Sync` is not implemented for `Cell<usize>`
note: captured value is not `Send`
8 |         println!("{}", document.root_element().html().len());
  |                        ^^^^^^^^ has type `Html` which is not `Send`
```



In other words, scraper's `Html` tree isn't thread safe as it uses `Cell` internally, so `tokio::spawn` refuses to take it. The easiest fix is to keep the parsing in the same task that downloaded the page.

But what about pages where the products only show up after a browser runs the javascript?

[Ultimate CSS Selector Cheatsheet for Web Scraping and HTML ParsingCSS selectors is a powerful HTML querying protocol which is used by browsers to determine what HTML elements to style. It's also incredibly useful in HTML parsing when web scraping or processing HTML data, as the same queries can be used to select values as well.](https://scrapfly.io/blog/posts/css-selector-cheatsheet)



Scrapfly

#### Scale your web scraping effortlessly

Scrapfly handles proxies, browsers, and anti-bot bypass — so you can focus on data.

[Try Free →](https://scrapfly.io/register)## 4. thirtyfour

Then we need a real browser. [thirtyfour](https://docs.rs/thirtyfour/0.37.5/thirtyfour/) is pretty much Selenium for Rust - it uses the same WebDriver protocol to open Chrome, load the page and run its javascript.

We don't have a ChromeDriver (or a [Selenium Grid](https://scrapfly.io/blog/posts/intro-to-web-scraping-using-selenium-grid)) running, so instead of connecting with `WebDriver::new` we'll use `WebDriver::managed`, which downloads the right driver and starts it for us. Let's add `thirtyfour = "=0.37.5"` and tokio to Cargo.toml:

rust

```rust
use thirtyfour::prelude::*;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let mut caps = DesiredCapabilities::chrome();
    caps.set_headless()?;
    let driver = WebDriver::managed(caps).await?;

    driver.goto("https://web-scraping.dev/products").await?;
    let products = driver.find_all(By::Css(".product h3 a")).await?;
    for product in &products {
        println!("{}", product.text().await?);
    }
    println!("products={}", products.len());

    driver.quit().await?;
    Ok(())
}
```



This opens headless Chrome, loads the products page and grabs every `.product h3 a` link:

text

```text
Box of Chocolate Candy
Dark Red Energy Potion
Teal Energy Potion
Red Energy Potion
Blue Energy Potion
products=5
```



All five products are there, and `managed` fetched a ChromeDriver for our Chrome 154 on its own. Note the `driver.quit()` at the end too - Rust can't run async code when the driver is dropped, so without it the browser gets closed by blocking the executor.



## 5. chromiumoxide

Do we need ChromeDriver at all? Not with chromiumoxide, which talks to Chrome over the Chrome DevTools Protocol (CDP) directly, though Chrome or Chromium still has to be installed. Let's add `chromiumoxide = "=0.9.1"`, tokio and `futures = "0.3"` to Cargo.toml:

rust

```rust
use chromiumoxide::{Browser, BrowserConfig};
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (mut browser, mut handler) = Browser::launch(BrowserConfig::builder().build()?).await?;
    let handle = tokio::spawn(async move {
        while let Some(event) = handler.next().await {
            if event.is_err() {
                break;
            }
        }
    });

    let page = browser.new_page("https://web-scraping.dev/products").await?;
    let products = page.find_elements(".product h3 a").await?;
    for product in &products {
        println!("{}", product.inner_text().await?.unwrap_or_default());
    }
    println!("products={}", products.len());

    browser.close().await?;
    browser.wait().await?;
    handle.await?;
    Ok(())
}
```



Here `Browser::launch` gives us a `Browser` and a `Handler`, and we poll the `Handler` in its own tokio task like the official example does, since no command reaches Chrome otherwise. At the end `browser.wait()` waits for Chrome to exit, so no zombie process is left behind.

Let's run it:

text

```text
Box of Chocolate Candy
Dark Red Energy Potion
Teal Energy Potion
Red Energy Potion
Blue Energy Potion
products=5
```



Why does the first build take so long? The CDP bindings are generated code, roughly 60,000 lines before macro expansion according to the docs.



## 6. dom\_smoothie

What if we just want the article text? dom\_smoothie finds the main content of a page by itself, so we don't write any selectors. It doesn't fetch pages, so we pair it with reqwest, adding `dom_smoothie = "=0.18.2"`, `reqwest = "=0.13.5"` and tokio to Cargo.toml:

rust

```rust
use dom_smoothie::Readability;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://web-scraping.dev/product/1";
    let html = reqwest::get(url).await?.error_for_status()?.text().await?;

    let mut readability = Readability::new(html, Some(url), None)?;
    let article = readability.parse()?;

    println!("title={}", article.title);
    println!("text_chars={}", article.text_content.chars().count());
    Ok(())
}
```



Running it prints:

text

```text
title=web-scraping.dev product Box of Chocolate Candy
text_chars=3022
```



That's the title plus 3,022 characters of page text.

Finding new urls to visit is a separate job, which our [scraper API vs crawler API](https://scrapfly.io/blog/posts/scraper-api-vs-crawler-api) comparison covers.

[Top Web Crawler Tools in 2026Web crawling has evolved dramatically in 2026. What used to require complex infrastructure and constant maintenance can now be accomplished with a few clicks or lines of code. But with so many options available, choosing the right tool can make or break your data collection project.](https://scrapfly.io/blog/posts/top-web-crawler-tools)



## FAQ

Can we scrape the /reviews page without a browser?Remember the review reqwest couldn't find? The page gets it from `/api/graphql`, and the query it sends sits in a `<script id="reviewsGQL">` tag. So we can post that query ourselves, with `serde_json` and reqwest's `json` feature:

rust

```rust
use scraper::{Html, Selector};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let html = reqwest::get("https://web-scraping.dev/reviews").await?.text().await?;
    let query: String = Html::parse_document(&html)
        .select(&Selector::parse("#reviewsGQL")?)
        .flat_map(|node| node.text())
        .collect();

    let body = json!({"query": query, "variables": {"first": 3}});
    let data: Value = reqwest::Client::new()
        .post("https://web-scraping.dev/api/graphql")
        .json(&body)
        .send()
        .await?
        .json()
        .await?;

    for edge in data["data"]["reviews"]["edges"].as_array().unwrap() {
        println!("{} {}", edge["node"]["date"], edge["node"]["text"]);
    }
    Ok(())
}
```



text

```text
"2023-05-18" "Unique flavor and great energy boost. It's the perfect gamer's drink!"
"2023-05-17" "Good flavor and keeps me energized. The bottle design is really fun."
"2023-05-16" "Excellent energy drink for gamers. The tropical flavor is refreshing."
```



That first one is the review our reqwest check reported as `found=false`. Our [scraping GraphQL](https://scrapfly.io/blog/posts/web-scraping-graphql-with-python) tutorial has more on this.







Can I mix reqwest's blocking client with tokio?rust

```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // needs reqwest = { version = "=0.13.5", features = ["blocking"] }
    let html = reqwest::blocking::get("https://web-scraping.dev/products")?.text()?;
    println!("bytes={}", html.len());
    Ok(())
}
```



text

```text
Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.
```



It panics, so it's either `reqwest::get` with tokio or the blocking client without it.









## Summary

In this article, we've taken a look at six Rust web scraping libraries and APIs by scraping the same web-scraping.dev products page with each one. We used reqwest and scraper for plain html pages, thirtyfour and chromiumoxide for pages that need javascript and dom\_smoothie for pulling out the page text.

For this, we used Rust with tokio, and to avoid being blocked we used ScrapFly's Rust SDK with `unblocker(true)`, since plain reqwest only got a "You've been blocked" page with a 200 status. For more on ScrapFly see our [documentation](https://scrapfly.io/docs/sdk/rust) and try it out for free!



### Web Scraping API

Scrape any website with our powerful API. Anti-bot bypass, JavaScript rendering, and rotating proxies built-in.



[Try Web Scraping API](https://scrapfly.io/docs/scrape-api/getting-started)



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   [  Add as a preferred source ](https://google.com/preferences/source?q=scrapfly.io) Table of Contents















 

  Table of Contents

- [Which Rust Web Scraping Library or API Is Best in 2026?](#which-rust-web-scraping-library-or-api-is-best-in-2026)
- [1. Scrapfly Web Scraping API](#1-scrapfly-web-scraping-api)
- [2. reqwest](#2-reqwest)
- [3. scraper](#3-scraper)
- [4. thirtyfour](#4-thirtyfour)
- [5. chromiumoxide](#5-chromiumoxide)
- [6. dom\_smoothie](#6-dom-smoothie)
- [FAQ](#faq)
- [Summary](#summary)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

 ## Related Articles

 [  

 python tools 

### 8 Top Python Web Scraping Libraries and APIs in 2026

Eight Python scraping options compared by the page-access, parsing, browser, crawling, JSON-query, and validation jobs t...

 

 ](https://scrapfly.io/blog/posts/top-10-web-scraping-libraries-in-python) [     

 ai tools 

### 5 MCP Servers for Web Scraping and Browser Automation in 2026

Compare five MCP servers by the work they can actually do: protected-site scraping, browser control, debugging, static f...

 

 ](https://scrapfly.io/blog/posts/best-mcp-servers-for-web-scraping) [  

 http python 

### Web Scraping with Python

Introduction tutorial to web scraping with Python. How to collect and parse public data. Challenges, best practices and ...

 

 ](https://scrapfly.io/blog/posts/web-scraping-with-python) 

  



   



 Scale your web scraping effortlessly, **1,000 free credits** [Start Free](https://scrapfly.io/register)