Ruby's standard library ships proxy support inside Net::HTTP, and it works, but the API is a row of positional arguments that is easy to get wrong, and one of those arguments defaults to reading your environment in a way that catches people off guard. Most application code reaches for Faraday on top, which has its own proxy option. This guide covers both, plus SOCKS5 through the socksify gem, authentication, rotating a pool, and the verification step that proves the proxy is carrying your traffic.
We run a proxy network and write the Ruby client for it, so the mistakes below are ones we have made ourselves: a Net::HTTP session that quietly used a shell proxy because the address argument defaulted to :ENV, a SOCKS proxy handed to a stdlib that does not speak SOCKS. Every example is copy-paste runnable. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns real, recently checked endpoints without a key.
How do you use a proxy with Ruby?
Build a Net::HTTP object with the proxy passed as constructor arguments after the host and port: Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass). For Faraday, pass the proxy option. The proxy is a property of the connection object, and for SOCKS you reach for the socksify gem because the standard library only speaks HTTP.
Net::HTTP: proxy arguments on the constructor
The full signature is Net::HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil). The proxy lives in p_addr through p_pass. Pass them explicitly and every request through that object goes via the proxy:
require 'net/http'
require 'uri'
uri = URI('https://httpbin.org/ip')
http = Net::HTTP.new(uri.host, uri.port,
'203.0.113.7', 8080, # proxy host, proxy port
'user', 'pass') # proxy user, proxy pass
http.use_ssl = (uri.scheme == 'https')
http.open_timeout = 5
http.read_timeout = 15
res = http.get(uri.request_uri)
puts res.body
Two things are worth spelling out. First, you set use_ssl yourself: Net::HTTP does not infer TLS from the URL. Once it is on, Net::HTTP opens a CONNECT tunnel through the HTTP proxy and performs TLS end to end with the origin, which is why a plain http:// proxy carries https:// traffic fine. Second, the user/pass arguments become the Proxy-Authorization header, so authenticated HTTP proxies need no extra code.
There is a second spelling using Net::HTTP::Proxy, which returns an anonymous subclass preconfigured with the proxy. It reads well when you want a reusable proxied client:
proxied = Net::HTTP::Proxy('203.0.113.7', 8080, 'user', 'pass')
proxied.start(uri.host, uri.port, use_ssl: true) do |http|
puts http.get(uri.request_uri).body
end
The :ENV default, and how to turn it off
Here is the argument that trips people up. When you call Net::HTTP.new(host, port) with no proxy arguments, p_addr is not nil, it is the symbol :ENV. That tells Ruby to read the lowercase http_proxy environment variable and use whatever it finds. So a request you thought was direct can silently route through a proxy set in your shell or CI:
# p_addr defaults to :ENV, so this reads http_proxy from the environment
http = Net::HTTP.new(uri.host, uri.port)
The URL in http_proxy has to include a scheme (http://host:port), and the accessors proxy_address, proxy_port, proxy_user, and proxy_pass let you inspect what got picked up. When you want a guaranteed-direct connection regardless of the environment, pass nil as the proxy address:
# nil disables the :ENV lookup: this connection never uses a proxy
http = Net::HTTP.new(uri.host, uri.port, nil)
Faraday
Most Ruby applications wrap Net::HTTP in Faraday. Faraday configures the proxy through its proxy option, which takes either a URL string or a hash:
require 'faraday'
# String form, credentials in the URL
conn = Faraday.new(
url: 'https://httpbin.org',
proxy: 'http://user:[email protected]:8080'
)
puts conn.get('/ip').body
The hash form keeps credentials out of the URL. Faraday's proxy options are :uri, :user, and :password:
conn = Faraday.new(url: 'https://httpbin.org') do |f|
f.proxy = {
uri: 'http://203.0.113.7:8080',
user: 'user',
password: 'pass',
}
end
Like the standard library it sits on, Faraday reads http_proxy from the environment when you do not pass the option, so set the proxy option explicitly whenever you want to be the one deciding where traffic goes.
SOCKS5 proxies with socksify
Net::HTTP has no SOCKS support at all, and neither does Faraday's default adapter. The community answer is the socksify gem, which patches Ruby's socket layer so ordinary connections tunnel through a SOCKS5 proxy:
gem install socksify
After requiring socksify/http, you get a Net::HTTP.socks_proxy factory that behaves like a proxied Net::HTTP session:
require 'socksify/http'
require 'uri'
uri = URI('https://httpbin.org/ip')
Net::HTTP.socks_proxy('198.51.100.14', 1080).start(uri.host, uri.port, use_ssl: true) do |http|
puts http.get(uri.request_uri).body
end
socks_proxy also accepts a username and password for SOCKS5 authentication. If you would rather route every TCP connection in the process, set the socket globals instead, though socksify's own README marks these globals as deprecated on Ruby 3.1 and later, so prefer the socks_proxy factory above for new code:
require 'socksify'
TCPSocket.socks_server = '198.51.100.14'
TCPSocket.socks_port = 1080
One privacy detail from socksify's own documentation matters here: it resolves hostnames through the proxy rather than locally, and exposes Socksify.resolve('example.com') for the same purpose. That is the remote-DNS behavior curl calls socks5h, so with socksify your local resolver never sees the names you visit. We cover why that matters in what is a SOCKS5 proxy.
Rotating across a pool
One IP sending every request is the exact shape a rate limiter watches for. Spread the load across a list and retry on a different proxy when one fails, because on any pool some exits are always down:
require 'net/http'
require 'uri'
POOL = [
['203.0.113.7', 8080],
['203.0.113.24', 3128],
['198.51.100.14', 8080],
].freeze
def get_rotating(uri, pool = POOL, tries: 4)
tries.times do
host, port = pool.sample
begin
http = Net::HTTP.new(uri.host, uri.port, host, port)
http.use_ssl = (uri.scheme == 'https')
http.open_timeout = 5
http.read_timeout = 15
return http.get(uri.request_uri)
rescue StandardError
next # dead or blocked exit, try the next one
end
end
raise "all #{tries} proxies failed for #{uri}"
end
puts get_rotating(URI('https://httpbin.org/ip')).body
You do not have to maintain the list by hand. Our free proxy API returns a fresh pool as plain text you can parse straight into that array:
raw = Net::HTTP.get(URI('https://hproxy.com/api/proxy-list?format=txt&protocol=http&recent=true&limit=50'))
pool = raw.split("\n").map(&:strip).reject(&:empty?).map do |line|
host, port = line.split(':')
[host, port.to_i]
end
For production, a rotating gateway that hands you one endpoint and a fresh residential IP per request removes list management entirely, but rotating a list by hand is the best way to understand what that gateway does for you.
Verify the exit IP
Never assume a proxy is working. Confirm the target sees the proxy's address and not yours, and do it before you trust an exit with real traffic:
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://httpbin.org/ip')
real = JSON.parse(Net::HTTP.get(uri))['origin']
http = Net::HTTP.new(uri.host, uri.port, '203.0.113.7', 8080)
http.use_ssl = true
proxied = JSON.parse(http.get(uri.request_uri).body)['origin']
puts "real: #{real}"
puts "proxy: #{proxied}"
abort 'proxy is not changing your IP' if real == proxied
If the two IPs match, the request never went through the proxy, usually because the proxy argument was nil or the address landed in the no-proxy list. It is also worth checking what headers arrive at https://httpbin.org/headers, because a transparent proxy can forward your real address in X-Forwarded-For even while origin looks changed. Our proxy checker runs that whole battery, exit IP, anonymity grade, real geolocation, and latency, in one paste if you would rather not script it.
Where to go from here
Two habits keep Ruby proxy code alive unattended: set open_timeout and read_timeout so a silent proxy cannot hang the thread, and verify every exit before you trust it. For a live pool to test against, the free proxy list re-verifies its entries every few minutes, and the free API hands them to you with no key.
From here, the cURL guide is the fastest way to sanity-check a proxy from the shell before you wire it into Ruby, the Python requests guide is the closest sibling if your stack spans both languages, and proxies for web scraping covers choosing the right proxy type and the request hygiene no HTTP client can add for you. When a project graduates from experiments to production, get IPs nobody else is burning on our paid pools.
Sources
- Ruby documentation: Net::HTTP: the
Net::HTTP.new(address, port, p_addr, p_port, p_user, p_pass, p_no_proxy)signature, the:ENVdefault that readshttp_proxy,Net::HTTP::Proxy, and theproxy_address/proxy_port/proxy_user/proxy_passaccessors. - Faraday documentation: the
proxyconnection option as a URL string or a hash of:uri,:user, and:password, and thehttp_proxyenvironment fallback (behavior confirmed in Faraday'sConnectionsource). - socksify gem README:
Net::HTTP.socks_proxy, theTCPSocket.socks_server/socks_portglobals, andSocksify.resolvefor proxy-side DNS resolution.