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.
# Drop ipfly_sdk.rb directly into your source tree, e.g.: cp ipfly_sdk.rb lib/ipfly_sdk.rb
curl -O https://ipfly.world/libs/ruby-sdk/ipfly_sdk.rb # or wget https://ipfly.world/libs/ruby-sdk/ipfly_sdk.rb
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.
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}"
endClient Options
All options are keyword arguments to IPFly::Client.new(token, **options).
| Option | Type | Default | Description |
|---|---|---|---|
token (positional) | String | required | Your IPFly API token. |
base_url | String | ipfly.world/api | Override to point at your own backend proxy. |
include | String | nil | Default include value sent with every request (e.g. "security"). Overridable per call. |
timeout | Numeric | 8.0 | Per-request timeout in seconds. |
retries | Integer | 2 | Retry attempts for network errors and 5xx. 4xx is never retried. |
retry_delay | Numeric | 0.3 | Base delay (seconds) for exponential backoff; jitter is added automatically. |
rate_limit | Numeric | 0 | Max requests/second this client will issue. 0 = unlimited. |
concurrency | Integer | 6 | Default max worker threads for batch_lookup. |
cache | Hash | see below | Response cache settings — see Cache Options. |
debug | Boolean | false | Print internal lifecycle events to $stderr (tokens are redacted). |
on_request | #call | nil | ->(ip, url) { } — fired right before a network request is sent. |
on_response | #call | nil | ->(ip, data, from_cache) { } — fired after a successful lookup. |
on_error | #call | nil | ->(ip, error) { } — fired when a lookup ultimately fails, after retries. |
on_cache_hit | #call | nil | ->(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 }).
| Key | Type | Default | Description |
|---|---|---|---|
:enabled | Boolean | true | Set false to disable caching entirely. |
:ttl | Numeric | 300 | How long a cached response stays valid, in seconds. |
:max_size | Integer | 500 | Max cached entries before least-recently-used entries are evicted. |
:storage | Symbol | :memory | :memory or :file. :file persists the cache to a JSON file across process runs. |
:path | String | .ipfly_cache.json | File 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.
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.
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
endBatchResult is a Struct: ip, ok (also available as #ok?), data, error.
Cache Controls
| Method | Returns | Description |
|---|---|---|
#clear_cache | nil | Empties the response cache immediately. |
#cache_stats | Hash | Returns { 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.
IPFly.valid_ip?('8.8.8.8') # true
IPFly.valid_ip?('2001:4860:4860::8888') # true
IPFly.valid_ip?('not-an-ip') # falseResponse 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.
{
"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.
| Code | Meaning |
|---|---|
| MISSING_TOKEN | No token was supplied when creating the client. |
| INVALID_IP | The IP string failed local validation before any request was sent. |
| TIMEOUT | The request exceeded timeout. |
| NETWORK_ERROR | The request failed before a response was received (offline, DNS, connection refused, etc). |
| HTTP_ERROR | The 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_RESPONSE | The server responded but the body wasn't valid JSON. |
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
endPerformance 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:
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
endExamples
Three common integration patterns: enriching a CSV of visitor IPs in bulk, a Sinatra proxy, and a Rails controller.
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
endrequire '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
endclass 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
endIn 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.