Tutorial6 min read

Ruby Web Scraping

Short answer

Ruby web scraping usually takes two gems: HTTParty (or Faraday, or the built-in Net::HTTP) to download a page, and Nokogiri to parse it with CSS selectors or XPath. For pages that need JavaScript, Ferrum, Selenium or the Playwright client drive a real browser from Ruby, and the same code runs inside a Rails background job. ProxyField residential proxies work with each of these gems over HTTP 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 9 sections
  1. The Ruby scraping toolkit
  2. Scrape a static page with HTTParty and Nokogiri
  3. Scrape multiple pages
  4. Errors, retries and rate limits
  5. Add a proxy
  6. Scrape JavaScript-rendered pages with Ruby
  7. Web scraping in Ruby on Rails
  8. Is Ruby a good choice for web scraping?
  9. Ruby Web Scraping FAQ

The Ruby scraping toolkit

GemWhat it doesVersion (RubyGems, 2026-10-23)License
NokogiriParses HTML and XML; CSS selectors and XPath1.19.4MIT
HTTPartySimple HTTP client with proxy options0.24.3MIT
FaradayHTTP client with middleware (retries, logging)2.15.0MIT
Net::HTTPHTTP client in Ruby's standard libraryships with RubyRuby license
MechanizeBrowser-like client: cookies, forms, following links2.14.1MIT
FerrumDrives Chrome over the DevTools protocol0.19.1MIT
selenium-webdriverSelenium's official Ruby bindings4.51.0Apache-2.0
playwright-ruby-clientRuby client for Playwright1.64.0MIT
KimuraiScraping framework on top of these2.2.0MIT

For most static pages, HTTParty plus Nokogiri is all you need.

Scrape a static page with HTTParty and Nokogiri

Shell
gem install httparty nokogiri
Ruby
require "httparty"
require "nokogiri"

url = "https://www.scrapethissite.com/pages/simple/"
response = HTTParty.get(url, headers: { "User-Agent" => "Mozilla/5.0" }, timeout: 30)
raise "HTTP #{response.code}" unless response.code == 200

doc = Nokogiri::HTML(response.body)
countries = doc.css("div.country").map do |card|
  {
    name: card.at_css("h3.country-name").text.strip,
    capital: card.at_css("span.country-capital").text.strip,
    population: card.at_css("span.country-population").text.to_i
  }
end
puts countries.size, countries.first

The page is a practice sandbox with 250 countries, each in a div.country. css returns every match and at_css the first; xpath and at_xpath are the XPath versions. Read an attribute with node["href"], and make relative links absolute with URI.join(url, href).to_s.

Scrape multiple pages

When the page number is a URL parameter, count up until a page comes back empty. On the sandbox's hockey table it is ?page_num=N:

Ruby
require "csv"

teams, page = [], 1
loop do
  res = HTTParty.get("https://www.scrapethissite.com/pages/forms/",
                     query: { page_num: page }, timeout: 30)
  rows = Nokogiri::HTML(res.body).css("tr.team")
  break if rows.empty?
  rows.each do |tr|
    teams << [tr.at_css("td.name").text.strip, tr.at_css("td.year").text.to_i, tr.at_css("td.wins").text.to_i]
  end
  page += 1
  sleep 1
end

CSV.open("teams.csv", "w") do |csv|
  csv << %w[team year wins]
  teams.each { |row| csv << row }
end

Where the site has a "Next" link instead, read doc.at_css("a[rel=next]")&.[]("href") and stop when it is nil. Mechanize does the same with page.link_with(text: "Next")&.click, and keeps cookies between pages.

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

Errors, retries and rate limits

Unattended Ruby scrapers fail in three predictable ways: timeouts, blocks and layout changes.

  • Retries with backoff: retry Net::OpenTimeout, Net::ReadTimeout and 429 or 5xx responses up to 3 times, sleeping 2, 4 and 8 seconds. With Faraday, the faraday-retry middleware does this for you.
  • Honor Retry-After on 429 responses, and keep a delay per host.
  • Fail loudly on nil: at_css returns nil when a selector matches nothing, and .text on nil raises NoMethodError. Check for nil and log the URL, so a layout change stops the run instead of writing empty rows.

Add a proxy

HTTParty takes the proxy as four options. 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.

Ruby
response = HTTParty.get("https://api.ipify.org",
  http_proxyaddr: "gate.proxyfield.io",
  http_proxyport: 7000,
  http_proxyuser: "<login>-country-us-city-new_york",
  http_proxypass: "<password>",
  timeout: 30)
puts response.body   # the IP the site sees

The same with Faraday and with Net::HTTP:

Ruby
require "faraday"
conn = Faraday.new(proxy: "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000")
puts conn.get("https://api.ipify.org").body

require "net/http"
uri = URI("https://api.ipify.org")
Net::HTTP.start(uri.host, uri.port, "gate.proxyfield.io", 7000,
                "<login>-country-us-city-new_york", "<password>", use_ssl: true) do |http|
  puts http.get(uri.request_uri).body
end

The gateway can give each request a new IP, or keep one IP for a sticky session from 30 s to 36 h, for multi-page flows that need the same visitor. Which IP type to use for which site: proxies for web scraping.

Scrape JavaScript-rendered pages with Ruby

Nokogiri does not run JavaScript. If the data is missing from response.body but visible in the browser, first look for the JSON request the page makes (DevTools > Network > Fetch/XHR) and call it with HTTParty. If you need the rendered page, drive a browser.

Ferrum controls Chrome directly, without a separate driver:

Ruby
require "ferrum"

browser = Ferrum::Browser.new(timeout: 30, proxy: {
  host: "gate.proxyfield.io", port: 7000,
  user: "<login>-country-us-city-new_york", password: "<password>"
})
browser.go_to("https://example.com/js-page")
html = browser.body              # the HTML after scripts ran
browser.quit
puts Nokogiri::HTML(html).at_css("h1")&.text

Playwright in Ruby goes through the playwright-ruby-client gem, which uses the Playwright driver from npm (npm install playwright and npx playwright install chromium first):

Ruby
require "playwright"

Playwright.create(playwright_cli_executable_path: "npx playwright") do |playwright|
  playwright.chromium.launch(headless: true, proxy: {
    server: "http://gate.proxyfield.io:7000",
    username: "<login>-country-us-city-new_york", password: "<password>"
  }) do |browser|
    page = browser.new_page
    page.goto("https://example.com/js-page")
    page.wait_for_selector("h1")
    puts page.text_content("h1")
  end
end

Selenium supports Ruby through the official selenium-webdriver gem. Chrome's --proxy-server flag takes no username or password, so with Selenium the simplest setup is an IP allowlist: add your server's IP to your account's allowlist, then pass --proxy-server=http://gate.proxyfield.io:<port>, where <port> is the port you set up in the dashboard for that IP. More on authenticated proxies in Selenium: Selenium proxy setup.

Browsers download every script, image and font, so they are slower and use more proxy traffic per page than plain HTTP; render only the pages that need it. The general approach is in how to scrape dynamic web pages.

Web scraping in Ruby on Rails

Never scrape inside a web request: a slow site would hold up your users. Put the scraper in a job and schedule it.

Ruby
# app/jobs/scrape_countries_job.rb
class ScrapeCountriesJob < ApplicationJob
  queue_as :default
  retry_on Net::ReadTimeout, wait: :polynomially_longer, attempts: 3

  def perform
    res = HTTParty.get("https://www.scrapethissite.com/pages/simple/", timeout: 30,
            http_proxyaddr: "gate.proxyfield.io", http_proxyport: 7000,
            http_proxyuser: ENV["PROXY_USER"], http_proxypass: ENV["PROXY_PASS"])
    Nokogiri::HTML(res.body).css("div.country").each do |card|
      Country.upsert({ name: card.at_css("h3.country-name").text.strip,
                       population: card.at_css("span.country-population").text.to_i },
                     unique_by: :name)
    end
  end
end

upsert with a unique index keeps reruns from duplicating rows. Schedule the job with your queue's recurring tasks: Solid Queue, the default queue backend in Rails 8, reads them from config/recurring.yml. Keep the proxy credentials in Rails credentials or environment variables.

Is Ruby a good choice for web scraping?

Yes, for small and medium jobs, and especially when the rest of your app is Ruby: Nokogiri is fast and mature, and HTTParty and Faraday make requests short to write. Python has the larger scraping ecosystem (Scrapy, more tutorials, more parsers), and Node.js has the most browser tooling; for very high page counts per server, compiled languages such as Rust or Go use less memory. Comparisons by language: choosing a language for web scraping. Other guides in this series: web scraping in Rust, how to build a web scraper.

Running Ruby scrapers at volume? ProxyField residential proxies: 412M+ residential IPs in 195+ countries, unlimited concurrent threads, pay per GB with no subscription. Start with the Easy Trial (1 GB); traffic never expires, and all purchases are final.

Ruby Web Scraping FAQ

How do I scrape a website with Ruby?

ProxyField residential proxies, from $1.95/GB, go into HTTParty as four options, and the scrape itself is short: fetch the page with HTTParty, parse it with Nokogiri::HTML(response.body), select elements with css or at_css, read .text.strip or node["href"], and write the rows with Ruby's CSV library. Loop over the pages with a short sleep, and route HTTParty through the rotating residential proxies once one IP is not enough.

Is Ruby good for web scraping?

Yes for most jobs: Nokogiri is fast and mature, and HTTParty or Faraday keep requests short. Python has the bigger scraping ecosystem and more tutorials, so Ruby makes most sense when the rest of your stack is Ruby or Rails.

Which Ruby gem is best for web scraping?

Nokogiri for parsing, paired with HTTParty or Faraday for requests; Mechanize when you need cookies, forms and link-following; Ferrum or the Playwright client for JavaScript pages. Kimurai bundles these into a scraping framework.

Does Nokogiri execute JavaScript?

No. Nokogiri parses the HTML it is given and never runs scripts. For script-built pages, call the JSON request the page makes, or render it with Ferrum, Playwright or Selenium and pass the resulting HTML to Nokogiri.

How do you scrape JavaScript-rendered pages with Ruby?

Drive a real browser: Ferrum controls Chrome over the DevTools protocol, the playwright-ruby-client gem runs Playwright, and selenium-webdriver runs Selenium. Wait for the element you need, then read the page's HTML or the element's text.

How do I use Playwright in Ruby?

Install the playwright-ruby-client gem and the Playwright driver from npm, then call Playwright.create(playwright_cli_executable_path: "npx playwright") and launch Chromium inside the block. The launch options take a proxy hash with server, username and password.

Does Selenium support Ruby?

Yes. The selenium-webdriver gem is one of Selenium's official language bindings, and it drives Chrome, Firefox, Edge and Safari. Chrome's --proxy-server flag takes no credentials, so pair it with an IP allowlist or an authenticating setup.

How do you scrape multiple pages with Ruby?

Loop over the page parameter, such as ?page_num=1, 2, 3, until a page returns no items, or follow the rel=next link until it disappears. Sleep between requests and reuse one connection where you can.

How do I use a proxy for Ruby web scraping?

Pass http_proxyaddr, http_proxyport, http_proxyuser and http_proxypass to HTTParty, or a proxy URL to Faraday.new(proxy: ...). Net::HTTP takes the proxy host, port, user and password as arguments to Net::HTTP.start.

Can I scrape websites in Ruby on Rails?

Yes, in a background job rather than in a controller, so a slow site never holds up a web request. Use Active Job with retries, store rows with upsert on a unique key, and schedule the job with your queue's recurring tasks.

Show 1 more questionShow fewer questions

How should Ruby scrapers handle errors, retries and rate limits?

Retry timeouts and 429 or 5xx responses a few times with growing waits, honor Retry-After, and never retry a 403 in a tight loop. Treat a nil from at_css as a layout change and stop with a clear log line.

Easy Trial: 1 GB for $2.95.

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