{"id":1095,"date":"2026-06-22T09:35:21","date_gmt":"2026-06-22T09:35:21","guid":{"rendered":"https:\/\/ruby-doc.org\/blog\/?p=1095"},"modified":"2026-06-23T09:54:21","modified_gmt":"2026-06-23T09:54:21","slug":"proxy-rotation-in-ruby-the-patterns-that-actually-work","status":"publish","type":"post","link":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/","title":{"rendered":"Proxy Rotation in Ruby: The Patterns That Actually Work"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Most Ruby developers reach for a simple array of proxy URLs when they first add rotation to a scraper: shuffle the list, pick one, make the request. This works until volume grows, at which point the scraper starts returning partial results or silent failures that are hard to trace. Getting rotation right is less about finding the correct gem and more about four independent problems: how to configure proxies in your HTTP library, when to rotate versus when to hold a session, how to detect blocks reliably, and how to track proxy health so degraded addresses stop burning requests.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring a Proxy in Ruby&#8217;s HTTP Libraries<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The three HTTP libraries most commonly used in Ruby scraping \u2014 Net::HTTP, Faraday, and HTTParty \u2014 each take a different approach to proxy configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/ruby-doc.org\/stdlib-2.6\/libdoc\/net\/http\/rdoc\/Net\/HTTP.html\" type=\"link\" id=\"https:\/\/ruby-doc.org\/stdlib-2.6\/libdoc\/net\/http\/rdoc\/Net\/HTTP.html\">Net::HTTP<\/a> exposes proxy parameters directly on the connection constructor:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">require &#8216;net\/http&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">http = Net::HTTP.new(<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8216;target-site.com&#8217;, 443,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8216;proxy.example.com&#8217;, 8080, # proxy host and port<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8216;username&#8217;, &#8216;password&#8217; # proxy credentials<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">http.use_ssl = true<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">response = http.get(&#8216;\/data&#8217;)<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/lostisland\/faraday\">Faraday<\/a> accepts proxy configuration at the connection level. Explicit timeouts are important with proxies \u2014 a slow or unresponsive address will stall a thread indefinitely without them:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">require &#8216;faraday&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">conn = Faraday.new(&#8216;https:\/\/target-site.com&#8217;) do |f|<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.proxy = &#8216;http:\/\/username:password@proxy.example.com:8080&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.options.open_timeout = 10 # seconds to establish the proxy connection<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.options.timeout = 30 # seconds to receive the full response<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.adapter Faraday.default_adapter<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">response = conn.get(&#8216;\/data&#8217;)<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/jnunemaker\/httparty\">HTTParty<\/a> sets the proxy at the class level by default, though it also supports per-request proxy options passed directly to the HTTP method. The class-level pattern is most common for straightforward scraping:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">require &#8216;httparty&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">class Scraper<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">include HTTParty<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">http_proxy &#8216;proxy.example.com&#8217;, 8080, &#8216;username&#8217;, &#8216;password&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">base_uri &#8216;https:\/\/target-site.com&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">response = Scraper.get(&#8216;\/data&#8217;)<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Each library configures a proxy once per connection or class. The rotation logic lives above the library, not inside it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Pattern Most Developers Start With<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The simplest rotation approach samples from an array on each request:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">PROXIES = [<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8216;http:\/\/user:pass@proxy1.example.com:8080&#8217;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8216;http:\/\/user:pass@proxy2.example.com:8080&#8217;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8216;http:\/\/user:pass@proxy3.example.com:8080&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def fetch(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">proxy = PROXIES.sample<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Faraday.new(proxy: proxy).get(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">This fails at scale for two reasons. First, a small pool causes the same addresses to reappear within the target site&#8217;s detection window. If a site rate-limits by IP over a fifteen-minute period and you have six proxies making hundreds of requests per hour, each proxy surfaces repeatedly before the window clears. Second, random sampling gives you no control over distribution. Some addresses will be hit far more often than others in any given window \u2014 which makes it difficult to track per-proxy error rates, respect usage limits on specific addresses, or identify which proxies are degraded. In a high-volume scraper, that loss of observability compounds into a reliability problem.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Thread-Safe Proxy Rotator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A round-robin rotator with a Mutex handles concurrent access and distributes load evenly across the pool:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">class ProxyRotator<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def initialize(proxies)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@proxies = proxies.freeze<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@index = 0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock = Mutex.new<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def next_proxy<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock.synchronize do<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">proxy = @proxies[@index % @proxies.length]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@index += 1<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">proxy<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ROTATOR = ProxyRotator.new(PROXIES)<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Round-robin is preferable to random sampling when you need predictable per-address traffic volumes \u2014 it makes each proxy&#8217;s request count deterministic and easier to reason about when debugging block patterns or usage costs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rotation as Faraday Middleware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Faraday&#8217;s middleware stack is the cleanest integration point for rotation logic. A middleware component intercepts every outgoing request and injects the next proxy before the request is dispatched:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">class RotatingProxyMiddleware &lt; Faraday::Middleware<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def initialize(app, rotator)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">super(app)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@rotator = rotator<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def call(env)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">env[:request][:proxy] = { uri: URI.parse(@rotator.next_proxy) }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@app.call(env)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">conn = Faraday.new(&#8216;https:\/\/target-site.com&#8217;) do |f|<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.use RotatingProxyMiddleware, ROTATOR<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.adapter Faraday.default_adapter<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Scraper code makes normal Faraday calls with no knowledge of which proxy is active. Swapping the rotation strategy means changing the middleware, not the scraping logic. Note that proxy injection through env[:request] depends on the adapter in use \u2014 the built-in Net::HTTP adapter supports this pattern, but behaviour varies across other adapters. Verify against your specific setup before relying on this in production.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Sticky Sessions for Multi-Step Workflows<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some scraping tasks require sending several related requests from the same IP \u2014 paginating through server-side session state, following a multi-step product flow, or scraping content that varies based on prior interaction. Rotating the proxy between steps breaks session continuity.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A sticky session pool assigns a fixed proxy to each logical workflow and replaces it only on expiry:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">class StickyProxyPool<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SESSION_TTL = 300 # seconds<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def initialize(proxies)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@proxies = proxies<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@sessions = {}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock = Mutex.new<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def proxy_for(session_id)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock.synchronize do<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">entry = @sessions[session_id]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">if entry.nil? || Time.now &#8211; entry[:assigned_at] &gt; SESSION_TTL<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@sessions[session_id] = { proxy: @proxies.sample, assigned_at: Time.now }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@sessions[session_id][:proxy]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The session ID is whatever identifies a logical unit of work: a product ID, a user flow, a scraping task key. Rotation happens at the workflow boundary, not between individual requests.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Block Detection Beyond Status Codes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Relying on HTTP status codes alone to detect blocks misses a common and frustrating pattern: many sites return 200 with a CAPTCHA challenge, an access-denied message, or a bot-detection interstitial embedded in the body. A scraper that does not check the response body will record these as successes and write garbage data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A blocked? helper that inspects both status and body catches these cases:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">BLOCK_STATUSES = [403, 429, 503].freeze<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">BLOCK_PATTERNS = [<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\/access.?denied\/i,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\/captcha\/i,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\/rate.?limit\/i,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\/unusual.?traffic\/i,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\/please.?verify\/i<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">].freeze<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def blocked?(response)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">return true if BLOCK_STATUSES.include?(response.status)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">BLOCK_PATTERNS.any? { |pat| response.body.to_s.match?(pat) }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The patterns here are starting points \u2014 build them from actual blocked responses observed against your target sites rather than assumptions. False positives on body patterns waste proxies; false negatives produce corrupt data. Logging the full response body when a block is detected makes calibration straightforward.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With a block? helper in place, the failure-based rotation function becomes:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">def fetch(url, rotator, attempts: 3)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">attempts.times do<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">proxy = rotator.next_proxy<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">response = Faraday.new(&#8216;https:\/\/target-site.com&#8217;, proxy: proxy).get(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">return response unless blocked?(response)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">raise &#8216;All proxies returned block responses for #{url}&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rescue Faraday::ConnectionFailed, Faraday::TimeoutError<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">retry if (attempts -= 1) &gt; 0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">raise<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Proxy Health Scoring and Pool Maintenance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A round-robin or random rotator has no concept of proxy health \u2014 it will keep dispatching requests through an address that has been blocked, timed out repeatedly, or become unreachable. In production this surfaces as a sustained drop in success rate that is hard to attribute without per-proxy tracking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A health-aware pool tracks failure counts per proxy, excludes addresses that exceed a failure threshold from rotation, and exposes a stats method for observability:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">class ProxyPool<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">FAILURE_THRESHOLD = 3<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">HealthEntry = Struct.new(:url, :failures, :requests)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def initialize(proxies)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@entries = proxies.map { |u| HealthEntry.new(u, 0, 0) }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock = Mutex.new<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def acquire<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock.synchronize do<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">healthy = @entries.reject { |e| e.failures &gt;= FAILURE_THRESHOLD }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">raise &#8216;Proxy pool exhausted&#8217; if healthy.empty?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">entry = healthy.min_by { |e| e.requests } # least-used first<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">entry.requests += 1<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">entry.url<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def mark_success(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">update(url) { |e| e.failures = [e.failures &#8211; 1, 0].max }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def mark_failure(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">update(url) { |e| e.failures += 1 }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def stats<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock.synchronize do<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@entries.map do |e|<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rate = e.requests.zero? ? 0.0 : e.failures.fdiv(e.requests).round(3)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">{ url: e.url, requests: e.requests, failure_rate: rate,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">healthy: e.failures &lt; FAILURE_THRESHOLD }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">private<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def update(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">@lock.synchronize { yield @entries.find { |e| e.url == url } }<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Usage pairs the pool with the blocked? check:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">pool = ProxyPool.new(PROXIES)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def fetch_with_pool(url, pool)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">proxy = pool.acquire<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">response = Faraday.new(&#8216;https:\/\/target-site.com&#8217;, proxy: proxy).get(url)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">if blocked?(response)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">pool.mark_failure(proxy)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">raise &#8216;Blocked response from #{proxy}&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">pool.mark_success(proxy)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">response<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rescue Faraday::Error =&gt; e<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">pool.mark_failure(proxy)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">raise<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The least-used selection inside acquire distributes load more evenly than strict round-robin when proxies have different failure counts \u2014 a partially degraded pool stays balanced rather than piling requests onto a shrinking set of healthy addresses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Calibrating FAILURE_THRESHOLD requires observing actual failure patterns against your target sites. A threshold of three handles transient network errors without discarding addresses prematurely; a threshold of one would burn proxies on single timeouts. Call pool.stats periodically \u2014 ideally logging it to your monitoring system \u2014 to track failure rates per address, identify proxies that are consistently degraded, and decide when the pool needs replenishing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When to Stop Managing Rotation Yourself<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The pool management problem scales faster than the scraping logic. For a Ruby scraper processing tens of thousands of pages per day, sourcing residential IPs in the volumes required to stay below per-address detection thresholds, validating them against target sites, and cycling addresses as they age out becomes substantial infrastructure work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For high-volume production scrapers, the practical alternative is a managed rotation provider. Providers like NetNut offer&nbsp;<a href=\"https:\/\/netnut.io\/rotating-proxies\/\">rotating proxies<\/a>&nbsp;backed by a large residential and ISP network, with rotation handled at the connection layer. Depending on account configuration, rotation can happen per request or per session \u2014 most managed providers support both modes. The Ruby integration reduces to a standard proxy configuration:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"># Rotation is handled server-side.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Your application connects through a single endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">conn = Faraday.new(&#8216;https:\/\/target-site.com&#8217;) do |f|<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.proxy = {<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">uri: &#8216;http:\/\/gw.netnut.io:7777&#8217;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">user: ENV[&#8216;NETNUT_USER&#8217;],<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">password: ENV[&#8216;NETNUT_PASS&#8217;]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.options.open_timeout = 10<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.options.timeout = 30<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">f.adapter Faraday.default_adapter<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">end<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Pool sizing, address validation, and rotation strategy are handled server-side. The blocked? check and response validation in your application code remain relevant \u2014 a managed provider handles IP rotation, but block detection and response quality checks stay in the scraper.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Matching Pattern to Task<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Different scraping tasks call for different combinations of these patterns. High-volume stateless retrieval \u2014 price monitoring, rank tracking, availability checks \u2014 suits Faraday middleware with a ProxyPool and per-request rotation. Multi-step workflows need StickyProxyPool keyed by workflow ID with rotation at the boundary. Scrapers against targets with unpredictable block rates benefit from failure-based rotation inside fetch_with_pool, where mark_failure drives the health scoring.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These patterns are composable. A Sidekiq job doing stateless product checks uses RotatingProxyMiddleware backed by a ProxyPool. A separate job running multi-page session scrapes uses StickyProxyPool. Both can share the same underlying pool instance and the same blocked? detection logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Proxy rotation in Ruby is not a library selection problem \u2014 Net::HTTP, Faraday, and HTTParty all handle proxies cleanly. The decisions that determine whether a scraper holds up at scale are above the library: pool size and IP quality, rotation granularity relative to session requirements, block detection that reads the response body and not just the status code, and health tracking that removes degraded addresses before they corrupt your data or your metrics.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Most Ruby developers reach for a simple array of proxy URLs when they first add rotation to a scraper: shuffle the list, pick one, make the request. This works until volume grows, at which point the scraper starts returning partial results or silent failures that are hard to trace. Getting rotation right is less about [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":1097,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1095","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby-tips"],"blocksy_meta":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.9 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Proxy Rotation in Ruby: The Patterns That Actually Work - Ruby-Doc.org<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Proxy Rotation in Ruby: The Patterns That Actually Work - Ruby-Doc.org\" \/>\n<meta property=\"og:description\" content=\"Most Ruby developers reach for a simple array of proxy URLs when they first add rotation to a scraper: shuffle the list, pick one, make the request. This works until volume grows, at which point the scraper starts returning partial results or silent failures that are hard to trace. Getting rotation right is less about [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/\" \/>\n<meta property=\"og:site_name\" content=\"Ruby-Doc.org\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-22T09:35:21+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-23T09:54:21+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2026\/06\/Proxy-Rotation-in-Ruby.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1536\" \/>\n\t<meta property=\"og:image:height\" content=\"1024\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Ryan McGregor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Ryan McGregor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/\"},\"author\":{\"name\":\"Ryan McGregor\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#\\\/schema\\\/person\\\/db7fcc3c518c40f29f8bf79ffa678dfc\"},\"headline\":\"Proxy Rotation in Ruby: The Patterns That Actually Work\",\"datePublished\":\"2026-06-22T09:35:21+00:00\",\"dateModified\":\"2026-06-23T09:54:21+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/\"},\"wordCount\":1838,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Proxy-Rotation-in-Ruby.png\",\"articleSection\":[\"Ruby tips\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/\",\"name\":\"Proxy Rotation in Ruby: The Patterns That Actually Work - Ruby-Doc.org\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Proxy-Rotation-in-Ruby.png\",\"datePublished\":\"2026-06-22T09:35:21+00:00\",\"dateModified\":\"2026-06-23T09:54:21+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#primaryimage\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Proxy-Rotation-in-Ruby.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Proxy-Rotation-in-Ruby.png\",\"width\":1536,\"height\":1024,\"caption\":\"Proxy Rotation in Ruby\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/proxy-rotation-in-ruby-the-patterns-that-actually-work\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Proxy Rotation in Ruby: The Patterns That Actually Work\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/\",\"name\":\"Ruby-Doc.org\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#organization\",\"name\":\"Ruby-Doc.org\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/wp-content\\\/uploads\\\/2025\\\/07\\\/Ruby-Doc.org_logo_cropped.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/wp-content\\\/uploads\\\/2025\\\/07\\\/Ruby-Doc.org_logo_cropped.png\",\"width\":909,\"height\":833,\"caption\":\"Ruby-Doc.org\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/#\\\/schema\\\/person\\\/db7fcc3c518c40f29f8bf79ffa678dfc\",\"name\":\"Ryan McGregor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f7b4d11da7f55d40163cd9431935ce1148d9bd69c95928064822f7757b6314dd?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f7b4d11da7f55d40163cd9431935ce1148d9bd69c95928064822f7757b6314dd?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/f7b4d11da7f55d40163cd9431935ce1148d9bd69c95928064822f7757b6314dd?s=96&d=mm&r=g\",\"caption\":\"Ryan McGregor\"},\"url\":\"https:\\\/\\\/ruby-doc.org\\\/blog\\\/author\\\/ryan\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Proxy Rotation in Ruby: The Patterns That Actually Work - Ruby-Doc.org","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/","og_locale":"en_US","og_type":"article","og_title":"Proxy Rotation in Ruby: The Patterns That Actually Work - Ruby-Doc.org","og_description":"Most Ruby developers reach for a simple array of proxy URLs when they first add rotation to a scraper: shuffle the list, pick one, make the request. This works until volume grows, at which point the scraper starts returning partial results or silent failures that are hard to trace. Getting rotation right is less about [&hellip;]","og_url":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/","og_site_name":"Ruby-Doc.org","article_published_time":"2026-06-22T09:35:21+00:00","article_modified_time":"2026-06-23T09:54:21+00:00","og_image":[{"width":1536,"height":1024,"url":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2026\/06\/Proxy-Rotation-in-Ruby.png","type":"image\/png"}],"author":"Ryan McGregor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Ryan McGregor","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#article","isPartOf":{"@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/"},"author":{"name":"Ryan McGregor","@id":"https:\/\/ruby-doc.org\/blog\/#\/schema\/person\/db7fcc3c518c40f29f8bf79ffa678dfc"},"headline":"Proxy Rotation in Ruby: The Patterns That Actually Work","datePublished":"2026-06-22T09:35:21+00:00","dateModified":"2026-06-23T09:54:21+00:00","mainEntityOfPage":{"@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/"},"wordCount":1838,"commentCount":0,"publisher":{"@id":"https:\/\/ruby-doc.org\/blog\/#organization"},"image":{"@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2026\/06\/Proxy-Rotation-in-Ruby.png","articleSection":["Ruby tips"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/","url":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/","name":"Proxy Rotation in Ruby: The Patterns That Actually Work - Ruby-Doc.org","isPartOf":{"@id":"https:\/\/ruby-doc.org\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#primaryimage"},"image":{"@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2026\/06\/Proxy-Rotation-in-Ruby.png","datePublished":"2026-06-22T09:35:21+00:00","dateModified":"2026-06-23T09:54:21+00:00","breadcrumb":{"@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#primaryimage","url":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2026\/06\/Proxy-Rotation-in-Ruby.png","contentUrl":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2026\/06\/Proxy-Rotation-in-Ruby.png","width":1536,"height":1024,"caption":"Proxy Rotation in Ruby"},{"@type":"BreadcrumbList","@id":"https:\/\/ruby-doc.org\/blog\/proxy-rotation-in-ruby-the-patterns-that-actually-work\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/ruby-doc.org\/blog\/"},{"@type":"ListItem","position":2,"name":"Proxy Rotation in Ruby: The Patterns That Actually Work"}]},{"@type":"WebSite","@id":"https:\/\/ruby-doc.org\/blog\/#website","url":"https:\/\/ruby-doc.org\/blog\/","name":"Ruby-Doc.org","description":"","publisher":{"@id":"https:\/\/ruby-doc.org\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/ruby-doc.org\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/ruby-doc.org\/blog\/#organization","name":"Ruby-Doc.org","url":"https:\/\/ruby-doc.org\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2025\/07\/Ruby-Doc.org_logo_cropped.png","contentUrl":"https:\/\/ruby-doc.org\/blog\/wp-content\/uploads\/2025\/07\/Ruby-Doc.org_logo_cropped.png","width":909,"height":833,"caption":"Ruby-Doc.org"},"image":{"@id":"https:\/\/ruby-doc.org\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/ruby-doc.org\/blog\/#\/schema\/person\/db7fcc3c518c40f29f8bf79ffa678dfc","name":"Ryan McGregor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/f7b4d11da7f55d40163cd9431935ce1148d9bd69c95928064822f7757b6314dd?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/f7b4d11da7f55d40163cd9431935ce1148d9bd69c95928064822f7757b6314dd?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/f7b4d11da7f55d40163cd9431935ce1148d9bd69c95928064822f7757b6314dd?s=96&d=mm&r=g","caption":"Ryan McGregor"},"url":"https:\/\/ruby-doc.org\/blog\/author\/ryan\/"}]}},"_links":{"self":[{"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/posts\/1095","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/comments?post=1095"}],"version-history":[{"count":4,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/posts\/1095\/revisions"}],"predecessor-version":[{"id":1101,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/posts\/1095\/revisions\/1101"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/media\/1097"}],"wp:attachment":[{"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/media?parent=1095"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/categories?post=1095"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ruby-doc.org\/blog\/wp-json\/wp\/v2\/tags?post=1095"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}