Pricing Docs Contact Status Blog
Log in Get Free API Key →

Ruby SDK

A dependency-free Ruby client for the IPFly API — caching, retries, rate limiting, thread-safe de-duplication and batch lookups, built in.

Get started → API Reference

Overview

The IPFly Ruby SDK wraps the IPFly API in a single dependency-free file. It relies only on the standard library (net/http, json, ipaddr, monitor, thread) and handles caching, retries, rate limiting, and thread-safe request de-duplication so your integration code can stay focused on what to do with the response.

TTL + LRU caching

In-memory by default, or persisted to a JSON file across process runs for scripts and Rake tasks.

Automatic retries

Exponential backoff with jitter on network errors and 5xx responses. 4xx fails fast, no wasted retries.

Thread-safe de-duplication

Two threads looking up the same IP at the same moment share a single HTTP request.

Batch lookups

A bounded worker-thread pool looks up many IPs at once; a bad IP never fails the whole batch.

Client-side rate limiting

A token-bucket limiter smooths bursts so you don't blow through your plan's requests/second.

Zero gems

One file, standard library only. MRI, JRuby, or TruffleRuby. MIT licensed.

Where This SDK Belongs

This client is meant to run server-side — a Rails/Sinatra app, a Rake task, a Sidekiq worker, a plain script. It is not meant to be shipped inside a distributed application where your API token could be extracted.

If you need geolocation results in a browser-facing app, put this SDK behind your own backend endpoint and call that endpoint from the client (see the Sinatra/Rails proxy example below), or use the IPFly JavaScript SDK with a domain-restricted token for prototyping.

Installation

No gem is published — copy ipfly_sdk.rb into your project.

bash
# Drop ipfly_sdk.rb directly into your source tree, e.g.:
cp ipfly_sdk.rb lib/ipfly_sdk.rb
bash
curl -O https://ipfly.world/libs/ruby-sdk/ipfly_sdk.rb
# or
wget https://ipfly.world/libs/ruby-sdk/ipfly_sdk.rb
ruby
require_relative 'ipfly_sdk'

client = IPFly::Client.new('YOUR_TOKEN')

Requirements

Ruby 2.7+ (uses keyword arguments and Struct with a block; no newer syntax required). No gems — just the Ruby standard library. Works on MRI, JRuby, and TruffleRuby.

Quick Start

Create a client once, then call lookup wherever you need geolocation data.

ruby
require_relative 'ipfly_sdk'

client = IPFly::Client.new(
  'YOUR_TOKEN',
  include: 'security' # optional: request the security/ASN field set on every call
)

# Look up a specific IP
data = client.lookup('8.8.8.8')
puts "#{data['city']} #{data['country_name']} VPN: #{data['security']['is_vpn']}"

# Look up the caller's own IP (omit the ip argument)
me = client.lookup_self
puts "You appear to be in #{me['city']}"

# Handle errors explicitly
begin
  client.lookup('not-an-ip')
rescue IPFly::Error => e
  puts "#{e.code}: #{e.message}"
end

Client Options

All options are keyword arguments to IPFly::Client.new(token, **options).

OptionTypeDefaultDescription
token (positional)StringrequiredYour IPFly API token.
base_urlStringipfly.world/apiOverride to point at your own backend proxy.
includeStringnilDefault include value sent with every request (e.g. "security"). Overridable per call.
timeoutNumeric8.0Per-request timeout in seconds.
retriesInteger2Retry attempts for network errors and 5xx. 4xx is never retried.
retry_delayNumeric0.3Base delay (seconds) for exponential backoff; jitter is added automatically.
rate_limitNumeric0Max requests/second this client will issue. 0 = unlimited.
concurrencyInteger6Default max worker threads for batch_lookup.
cacheHashsee belowResponse cache settings — see Cache Options.
debugBooleanfalsePrint internal lifecycle events to $stderr (tokens are redacted).
on_request#callnil->(ip, url) { } — fired right before a network request is sent.
on_response#callnil->(ip, data, from_cache) { } — fired after a successful lookup.
on_error#callnil->(ip, error) { } — fired when a lookup ultimately fails, after retries.
on_cache_hit#callnil->(ip, data) { } — fired when a cached response is served instead of a network call.

Cache Options

Passed as the cache hash, e.g. IPFly::Client.new(token, cache: { storage: :file, ttl: 3600 }).

KeyTypeDefaultDescription
:enabledBooleantrueSet false to disable caching entirely.
:ttlNumeric300How long a cached response stays valid, in seconds.
:max_sizeInteger500Max cached entries before least-recently-used entries are evicted.
:storageSymbol:memory:memory or :file. :file persists the cache to a JSON file across process runs.
:pathString.ipfly_cache.jsonFile path used when storage: :file.

Rails note: with multiple Puma/Unicorn worker processes, :memory caching is per-process, same as PHP-FPM — it won't be shared across workers. Use :file storage, or see Extending the Cache to wrap Rails.cache.

lookup & lookup_self

#lookup(ip = nil, include: nil, skip_cache: false, timeout: nil) looks up a single IP. Pass nil (or omit) to geolocate the caller — or use the #lookup_self(**kwargs) shorthand.

ruby
client.lookup(
  '1.1.1.1',
  include: 'security', # overrides the client-level `include` for this call only
  skip_cache: true,     # force a fresh network request, bypassing the cache
  timeout: 3.0            # override the client's default timeout for this call
)

Returns the parsed JSON response as a Hash (see Response Shape). Raises IPFly::Error on failure (see Error Handling).

batch_lookup

#batch_lookup(ips, concurrency: nil, **kwargs) looks up an array of IPs using a bounded pool of worker threads (concurrency overrides the client default). This call never raises for an individual IP failure — it returns an array of IPFly::BatchResult in the same order as ips.

ruby
results = client.batch_lookup(['8.8.8.8', '1.1.1.1', 'not-an-ip'])
results.each do |r|
  if r.ok?
    puts "#{r.ip} -> #{r.data['city']}"
  else
    puts "#{r.ip} failed: #{r.error.code}"
  end
end

BatchResult is a Struct: ip, ok (also available as #ok?), data, error.

Cache Controls

MethodReturnsDescription
#clear_cachenilEmpties the response cache immediately.
#cache_statsHashReturns { enabled:, size:, ttl:, max_size: } — useful for logging or a health-check endpoint.

IPFly.valid_ip?()

A module-level helper that validates an IPv4 or IPv6 string without making a request.

ruby
IPFly.valid_ip?('8.8.8.8')               # true
IPFly.valid_ip?('2001:4860:4860::8888')  # true
IPFly.valid_ip?('not-an-ip')             # false

Response Shape

Successful lookups return the same JSON structure the IPFly API returns, unchanged, as a Ruby Hash with string keys (not symbols) — see the full field reference in the API docs. Shape depends on your plan and the include value requested.

ruby
{
  "ip" => "8.8.8.8",
  "hostname" => "dns.google",
  "country_name" => "United States",
  "city" => "Mountain View",
  "latitude" => "37.4056",
  "longitude" => "-122.0775",
  "time_zone" => { "name" => "America/New_York", "..." => "..." },
  "asn" => { "asn" => "AS15169", "name" => "Google LLC", "..." => "..." },
  "security" => { "is_vpn" => false, "is_tor" => false, "..." => "..." }
}

Error Handling

All failures raise IPFly::Error, a StandardError subclass carrying #message, #status, #code, and #cause.

CodeMeaning
MISSING_TOKENNo token was supplied when creating the client.
INVALID_IPThe IP string failed local validation before any request was sent.
TIMEOUTThe request exceeded timeout.
NETWORK_ERRORThe request failed before a response was received (offline, DNS, connection refused, etc).
HTTP_ERRORThe API responded with a non-2xx status — check #status (401 = bad token, 403 = plan/permission issue, 404 = private/bogon IP, 429/5xx = retry-worthy).
BAD_RESPONSEThe server responded but the body wasn't valid JSON.
ruby
client = IPFly::Client.new('YOUR_TOKEN')

begin
  client.lookup('8.8.8.8')
rescue IPFly::Error => e
  if e.code == 'TIMEOUT'
    # retry later, log, fall back to a default
  elsif e.status == 401
    # token is invalid — surface a config error, don't retry
  end
  puts e.message
end

Performance Notes

Caching

Avoids re-querying the same IP within the TTL window — handy for a request handler looking up the same visitor repeatedly, or a batch job re-processing overlapping log files.

De-duplication

If two threads call lookup('1.2.3.4') at the same moment, only one HTTP request goes out; both get the same result.

Rate limiting

A courtesy limiter on the client side that smooths bursts (e.g. a bulk-enrichment script) so you don't blow through your plan's requests/second in a tight loop.

Threads, not Ractor/Fiber

batch_lookup uses plain Thread objects in a worker pool — fine for I/O-bound HTTP calls even under MRI's GIL, which releases during blocking I/O. No extra concurrency gem required.

Extending the Cache

For a multi-process deployment (Puma/Unicorn workers, multiple dynos), implement the same interface as MemoryStore/FileStore (get, set, delete, keys, size, clear) over whatever shared store you already use — or simplest, wrap Rails.cache:

ruby
class RailsCacheStore
  def initialize(prefix: 'ipfly:')
    @prefix = prefix
  end

  def get(key)
    Rails.cache.read("#{@prefix}#{key}")
  end

  def set(key, value)
    Rails.cache.write("#{@prefix}#{key}", value)
  end

  def delete(key)
    Rails.cache.delete("#{@prefix}#{key}")
  end

  # Rails.cache doesn't support key enumeration by default, so LRU
  # eviction and #size are left to your cache store's own expiry policy.
  def keys
    []
  end

  def size
    0
  end

  def clear
    nil
  end
end

Examples

Three common integration patterns: enriching a CSV of visitor IPs in bulk, a Sinatra proxy, and a Rails controller.

ruby
require 'csv'
require_relative 'ipfly_sdk'

client = IPFly::Client.new(ENV.fetch('IPFLY_TOKEN'), cache: { storage: :file, path: 'geo_cache.json' })

ips = CSV.read('visitors.csv', headers: true).map { |row| row['ip'] }

client.batch_lookup(ips, concurrency: 8).each do |result|
  if result.ok?
    puts "#{result.ip} -> #{result.data['country_name']}"
  else
    puts "#{result.ip} -> error: #{result.error.code}"
  end
end
ruby — Sinatra
require 'sinatra'
require_relative 'ipfly_sdk'

client = IPFly::Client.new(ENV.fetch('IPFLY_TOKEN'))

get '/api/geo' do
  content_type :json
  ip = params['ip'] || request.ip
  begin
    client.lookup(ip, include: 'security').to_json
  rescue IPFly::Error => e
    status e.status || 502
    { error: e.code, message: e.message }.to_json
  end
end
ruby — Rails controller
class GeoController < ApplicationController
  def show
    client = Rails.application.config.x.ipfly_client # memoize this in an initializer
    data = client.lookup(params[:ip] || request.remote_ip, include: 'security')
    render json: data
  rescue IPFly::Error => e
    render json: { error: e.code, message: e.message }, status: e.status || 502
  end
end

In both proxy examples, your frontend calls your own endpoint — the IPFly token stays in this process and is never sent to the client.

Requirements

Ruby 2.7 or later (uses keyword arguments and Struct with a block; no newer syntax required). No gems — just the standard library. Works on MRI, JRuby, and TruffleRuby with no native extensions.

License

MIT — use it, fork it, ship it.

Build on reliable IP Intelligence

Start with free requests per day. No credit card. No commitments. Scale when you’re ready.