Skip to main content
Web Scraping

Published Oct 22, 2026

What Is the Best Way to Build a Web Scraper in Rust?

Learn Rust web scraping with reqwest and scraper. Build paginated crawlers, add proxies, validate rows, and write dependable CSV output.

What Is the Best Way to Build a Web Scraper in Rust?

Quick Answer

Rust web scraping commonly pairs reqwest with the scraper crate for asynchronous requests and CSS-based HTML extraction. Reuse one client, set finite crawl limits, and define a complete record before writing output. Add a browser when JavaScript or interaction produces the required data.

Need proxy infrastructure for this workflow? Compare residential and mobile proxy options.

bash

Key Takeaways

  • reqwest handles asynchronous requests, connection pooling, redirects, cookies, timeouts, and proxy configuration.
  • The scraper crate parses returned HTML and queries its document tree with CSS selectors.
  • The tutorial follows pagination, detects repeated records, and writes a complete dataset atomically.
  • WebDriver fits pages where JavaScript or browser interaction produces the required values.
  • Larger Rust crawls need bounded tasks, per-host pacing, retry budgets, deduplication, and useful measurements.
  • Proxy routing should answer a geographic, route-diversity, or session requirement.

Which Rust Web Scraping Tool Should You Choose?

The right Rust scraping tool depends on the response format and the work required before extraction. Static HTML favors reqwest and scraper, while interactive pages may need WebDriver.

ToolPrimary roleBest forMain limitation
reqwestRequest clientAPIs, files, and custom HTTP workflowsDoes not query HTML elements
reqwest and scraperRequest and parseStatic HTML, CSS selectors, and paginated crawlsDoes not execute JavaScript
thirtyfour and WebDriverBrowser automationRendered content, interactions, and browser testingUses more memory, processing time, and bandwidth

Use reqwest alone for JSON, XML, files, or other non-HTML responses. Add the scraper crate when the required fields live inside HTML. This separation keeps transport and parsing responsibilities clear.

reqwest provides an asynchronous client with connection reuse, redirect policies, cookies, TLS, and proxy support. One long-lived client preserves its connection pool across related requests.

The scraper crate parses HTML and queries the document tree with CSS selectors. It fits server-rendered pages and saved response fixtures.

A WebDriver client controls a browser through a compatible driver. The headless browser guide explains its waits, bandwidth, versioning, and process-management costs.

The broader web scraping guide explains retrieval, parsing, and storage as separate stages. Rust coordinates them efficiently, but each stage still needs a clear contract.

Synthetic throughput alone should not decide the stack. Compare target behavior, development time, parsing needs, observability, deployment size, and maintenance. A browser adds little when direct requests already return complete data.

What Do You Need Before Scraping With Rust?

A Rust scraper needs a supported toolchain, a target you can inspect, and a defined record shape. This tutorial uses stable Rust and a public practice website.

This example passed compilation, linting, tests, and a live run with Rust 1.99.1. The version appears in the official Rust release announcements. Install Rust through the official installer, then confirm both tools:

bash

The finished program collects hockey rows from `https://www.scrapethissite.com/pages/forms/`. Each record contains a team name, year, wins, losses, and final source URL. The verified run followed six pages and saved 582 records.

Examine one row and the active next-page control before choosing selectors. The parser expects `tr.team`, `.name`, `.year`, `.wins`, `.losses`, and `a[aria-label='Next']`. Adapt those selectors and markers for another source.

Write the record contract before the crawler. Decide which fields are required, which values are acceptable, and what forms a duplicate. The parser can then return a clear outcome for each row.

Set the host allowlist, page ceiling, body cap, request delay, and mandatory fields before the first request. Keep a sanitized HTML fixture for repeatable parser tests.

Update the example user-agent contact URL before changing the target. Establish direct access before adding routing. The web scraping with proxies guide explains where proxies fit.

How Do You Install Rust Web Scraping Dependencies?

Cargo resolves the Rust dependencies and records exact versions in Cargo.lock. This project enables features for cookies, decompression, signals, timing, and serialization.

Create a binary project and enter its directory:

bash

Add the versions used for this tutorial:

bash

Cargo writes direct requirements to `Cargo.toml` and resolved versions to `Cargo.lock`. Commit both files for an application. Production builds should use `--locked` against the reviewed graph.

reqwest supplies transport, cookies, decompression, and optional proxy support. scraper supplies HTML parsing and selectors. The remaining crates cover errors, decoding, media types, CSV output, temporary files, and async execution.

Tokio runs reqwest's asynchronous client and supplies timers and signal handling. This tutorial keeps requests sequential before introducing task concurrency.

Cargo features change a crate's compiled capabilities and dependency graph. reqwest needs the matching features for cookies and each decompression method. Removing unused features can reduce build time and review scope.

Pinned dependencies still need maintenance. Review release notes, scan the lockfile for advisories, and rerun extraction tests before accepting an update.

How Do You Build a Rust Scraper With reqwest and scraper?

The Rust scraper separates requests, parsing, pagination, and storage while reusing one reqwest client. Each responsibility remains visible inside one runnable file.

  1. Create one client: Configure cookies, timeouts, decompression, redirects, pooling, and optional proxy routing.
  2. Fetch each page: Follow a finite number of same-host HTTPS redirects and cap the response body.
  3. Parse the records: Require every selected field, reject malformed numbers, and identify duplicates with a stable key.
  4. Control pagination: Track requested and final URLs, pause between pages, and enforce a page ceiling.
  5. Write the output: Serialize CSV fields, neutralize formula-like values, and replace the file after a complete write.

Replace `src/main.rs` with this code:

bash

The host check keeps redirects and pagination links on the intended source. Update both `START_URL` and `ALLOWED_HOST` when adapting the program.

The code handles redirects manually so each destination passes the host check. It streams responses into a bounded buffer, enforcing five MiB while bytes arrive.

The decoder follows the response charset and otherwise assumes UTF-8. Legacy pages may declare encoding only inside HTML, requiring another detection step.

The parser catches missing values, malformed numbers, duplicate team-year keys, duplicate next links, and pagination loops. Its CSV writer creates a temporary file beside the final output. An interrupted run leaves the previous complete file untouched.

The `Selectors` structure parses fixed selectors once instead of rebuilding them for every row. Three unit tests cover a complete row, formula-like text, and an unexpected host. Add fixtures for every supported page variant.

How Do You Run and Validate the Rust Scraper?

A Rust scraper should pass formatting, linting, tests, dependency review, and a controlled live run before production. Each check answers a different question about the build or source.

Run these commands inside `rust-scraper`:

bash

The tested program passed formatting, warning-free linting, three unit tests, and the RustSec audit. Its live run saved 582 rows across six pages. Treat that count as a reviewed test expectation.

Open `teams.csv` and inspect its header, row count, required fields, source URLs, and record keys. Compare several rows with their source pages.

Test a broken selector, non-HTML response, oversized body, unexpected redirect host, and repeated pagination URL. Cancel one run and confirm the earlier output remains usable.

Compare a second run with the first while the source remains unchanged. Both runs should produce identical keys. Automated checks can also enforce schema and expected record ranges.

The sample stores complete source URLs and may include them in errors. Adapted crawlers should redact sensitive queries, credentials, and response bodies. The how to test proxies guide separates routing, source, and parser failures.

How Do You Use a Proxy With reqwest?

A reqwest proxy belongs on `ClientBuilder` before the reusable connection pool exists. The program reads one HTTP endpoint and optional credentials from environment variables.

Set the values in Bash without placing the password in source code:

bash

Leave both credential variables unset for source-IP authentication. The program treats empty values as unset and reports incomplete settings, invalid hosts, invalid text, and invalid ports during startup.

reqwest reads system proxy settings by default. The builder calls `.no_proxy()` before adding the configured endpoint. This keeps inherited development settings from changing the route.

This program configures an HTTP proxy endpoint for HTTP and HTTPS destinations. SOCKS5 requires reqwest's `socks` feature. Use `socks5h://` when hostname resolution should occur through the proxy.

Keep credentials out of source files, repository URLs, screenshots, and error messages. Deployed programs should retrieve them from an approved secret manager.

Client reuse affects connection pooling and observed route changes. Test exits with the same client lifetime and session settings planned for production. The proxy rotation guide explains that interaction.

When Does Rust Web Scraping Need a Browser?

Rust web scraping needs a browser when JavaScript, rendered state, or interaction produces the required data. Compare the original response with the rendered page before choosing WebDriver.

Inspect the initial HTML and relevant network responses. A rendered page may fetch structured data from a simpler endpoint. Prefer an official API when it supports the required workload.

Rust WebDriver clients, including thirtyfour, control compatible browsers through a separate driver process. They can wait for selectors, interact with controls, preserve state, and capture screenshots. Plan for browser versions, driver compatibility, and process cleanup.

Wait for an observable selector, response, or application state instead of sleeping for a fixed duration. A load event may precede the required asynchronous content.

Isolate browser sessions when cookies or storage must not cross jobs. Cap simultaneous pages, close sessions after finite tasks, and record the browser version. Include viewport, locale, and route in test evidence.

Blocking images or fonts can reduce transferred bytes and alter page behavior. Compare the resulting records before applying that setting across a workload. Keep resources needed for layout, authentication, or extraction.

What Common Rust Web Scraping Errors Should You Check?

Rust scraping errors are easier to resolve when transport, parsing, pagination, record, and storage failures remain separate. Identify the failing stage before changing selectors, routes, or retries.

SymptomLikely causeFirst check
`407 Proxy Authentication Required`Wrong credentials or missing source-IP approvalTest the endpoint and authentication mode without printing secrets
`429 Too Many Requests`Request pressure or destination policyHonor `Retry-After`, reduce concurrency, and review authorization
HTTP 200, but no recordsConsent page, wrong locale, soft block, or selector driftInspect sanitized HTML and expected markers
Repeated page URLBroken next-link logic or redirect loopCompare normalized requested and final URLs
TimeoutSlow route, slow destination, or oversized workRecord connection, headers, body, and parser timing separately
Garbled textWrong charset or decoding assumptionInspect `Content-Type`, raw bytes, and declared document encoding
Duplicate recordsOverlapping pagination or unstable orderingUse a source-specific key and preserve source evidence
CSV opens incorrectlyMissing escaping, encoding mismatch, or formula-like fieldsTest commas, quotes, newlines, Unicode, and leading formula characters

Retry repeatable temporary failures with a small attempt cap, exponential backoff, jitter, and any stated delay. Authentication errors, unexpected media types, and rejected records need diagnosis.

Record a specific reason for each failed page and discarded record. A proxy pool adds route health data, while selector failures remain parser problems.

Rust compiler errors often reveal ownership or lifetime mistakes before a request runs. Read the complete diagnostic before changing the data model. Unnecessary clones can obscure the underlying design problem.

How Do You Scale Rust Web Scraping Safely?

Rust scraping scales through bounded tasks, per-host pacing, finite queues, and shared retry budgets. Raising task counts can increase failures faster than useful output.

Enable Tokio's `sync` feature, then use a semaphore, bounded channel, or fixed worker set. Give each destination its own limit. Increase workers after reviewing latency, responses, and completed records.

Reuse one reqwest client for jobs sharing a route, cookie boundary, TLS policy, headers, and timeouts. Create another client when those boundaries differ. A client per request discards connection reuse.

With scraper's default features, the document tree is neither `Send` nor `Sync`. A future passed to `tokio::spawn` cannot hold it across an `.await`. Finish parsing between await points and move owned records across task boundaries.

Give one layer ownership of each retry class and enforce a shared attempt budget. Uncoordinated request, page, and job retries can compound traffic. Cache unchanged responses and deduplicate queued URLs when source rules allow both.

Track requests, transferred bytes, statuses, latency, retries, rejected pages, duplicates, and completed records. Measure cost per completed record rather than cost per request.

Divide queued work by source and retain a stable key for every record. Persist checkpoints after complete batches reach durable storage. A restart can then resume unfinished work.

Measure parser time separately for large HTML documents. Async execution helps during I/O waits, not CPU-bound parsing. Move expensive parsing into bounded blocking tasks after profiling confirms the bottleneck.

What Rules Should a Rust Web Scraper Follow?

A Rust scraper should collect authorized data at a rate appropriate for each source. Assign clear ownership for permission, policy changes, and data quality.

Check applicable law, website terms, robots.txt directives, and any contract governing the source. Prefer an official API when it meets the requirement. Private or restricted data requires an appropriate basis and authorization.

Set a descriptive user agent when the source permits automated access. Publish a monitored contact page and keep request rates conservative. Honor `Retry-After` and documented source limits.

Limit collection to fields required for the documented purpose. Define retention and access controls before storage. Protect sensitive data and credentials according to the organization's security requirements.

A CAPTCHA, explicit denial, or repeated block should trigger review. Seek permission, use an approved feed, or reduce the collection scope before continuing.

Document approved sources, owners, schedules, fields, retention, and stop conditions. Revisit that record after a redesign, policy change, or new business purpose.

How Can Proxidize Support Rust Web Scraping?

Proxidize supplies controlled routes, location selection, and session behavior beneath a reqwest client. The Rust application retains ownership of requests, parsing, pacing, retries, and storage.

For Per GB plans, create one access point for each routing policy. Build its reqwest client once and reuse it across related requests. Separate clients keep cookie jars and connection pools apart.

Residential Proxies fit broad public-data collection across supported countries, cities, and ISPs. Rotating sessions suit independent observations. Sticky sessions maintain one route while related requests share cookies.

Best For: Residential Proxies fit global Rust crawls that need broad location coverage and independent route selection.

Mobile Proxies fit workloads whose result depends on a mobile-network route. Residential routes remain the natural starting point for global collection.

Best For: Mobile Proxies fit Rust tests that need a mobile-network route.

Connection reuse matters for both products. A session setting can select an exit while reqwest keeps an existing connection alive. Test route behavior with the planned client lifecycle.

Start with a direct connection, then add the proxy route required by the workload. Compare completed records, retries, transferred bytes, latency, and cost. Choose the product matching the required geography and network type.

What Should You Remember About Rust Web Scraping?

Rust gives scraping teams strong request, parsing, and concurrency tools. A dependable system still starts with a small crawler, finite work, and observable output.

  • Pair reqwest with scraper when the required values exist in the initial HTML.
  • Reuse each client within one route, cookie, and session boundary.
  • Add WebDriver when JavaScript or browser interaction creates the required data.
  • Bound concurrency, retries, pagination, queues, and response sizes before increasing volume.
  • Choose proxy routes from geography, network type, and session needs after direct extraction works.

Frequently asked questions

Rust suits scrapers that need native binaries, predictable memory use, and explicit concurrency control. Its crates cover requests, parsing, browser control, serialization, and asynchronous scheduling. Teams new to ownership and async code may need more development time before maintaining production crawlers confidently.

reqwest handles requests, pooling, cookies, timeouts, TLS, and proxies. scraper parses HTML with CSS selectors, while Tokio runs asynchronous work. CSV, Serde, and anyhow cover output, serialization, and error handling, but production crawls may also need persistent queues and monitoring.

reqwest fetches responses without executing page JavaScript. It fits sources whose initial response or documented data endpoint contains the required values. Choose WebDriver when rendering or interaction creates those values, then wait for a measurable page state before extracting records.

Create a reqwest `Proxy`, add it to `ClientBuilder`, and build the client before sending requests. Keep credentials in environment variables or a secret manager. Reuse that client within one route policy, then verify the observed exit and session behavior with a controlled test.

No universal concurrency value fits every Rust scraper or source. Start with one task, measure latency and completed records, then increase within documented limits. Bound tasks per host, share retry budgets, and reduce activity when errors, challenges, or rejected records begin rising.

Rust does not determine whether a collection workflow is lawful or permitted. Review applicable law, source terms, authorization, data rights, and contractual restrictions for each target. Prefer official APIs when suitable, collect necessary data, and obtain legal advice for sensitive or high-risk work.