Why Rails Apps Are Bot Targets
Ruby on Rails powers millions of web applications—from startup MVPs to enterprise platforms. Your Rails apps handle sensitive operations: user authentication, payment processing, API endpoints, form submissions, and account creation. Without bot detection, attackers can:
- Brute-force login forms with credential stuffing attacks (stolen username/password pairs)
- Scrape your Rails API endpoints at scale using Python, Node.js, or Puppeteer
- Perform fake account registration to resell accounts or commit fraud
- Spam your platform with auto-generated comments, reviews, or forum posts
- Abuse your free tier or API quota by making millions of requests
- Test stolen credit cards on your checkout flow
- Perform distributed brute-force attacks on your app
Rails developers often focus on building features first, leaving bot detection for later—or not at all. This guide shows you how to add real bot detection to Rails in 5 minutes—with working code, free tier access (1,000 daily verifications), Rack middleware patterns, and production best practices.
How Bot Detection Works
Device.AI's bot detection API accepts an HTTP request context (IP address, headers, request metadata) and returns a bot risk score (0.0 = bot, 1.0 = human). You decide what to do with the score: block, challenge with CAPTCHA, rate-limit, or allow.
Rails bot detection happens server-side via Rack middleware or controller filters. Device.AI analyzes:
- IP reputation and geolocation
- Request headers and fingerprints
- Known bot signatures (User-Agent strings, Puppeteer, Selenium, headless browsers)
- Request timing and behavioral patterns
- Proxy detection and VPN identification
Step 1: Get Your Free API Key (1 Minute)
Visit device.ai, click \"Get Free API Key\" (no signup required, no credit card), and copy your key. Your first 1,000 daily verifications are completely free.
You'll receive a key like:
dv_live_abc123def456ghi789jkl
Add it to your config/credentials.yml.enc or environment variables:
DEVICE_AI_API_KEY=dv_live_abc123def456ghi789jkl
Access it in Rails:
api_key = Rails.application.credentials.dig(:device_ai_api_key) || ENV['DEVICE_AI_API_KEY']
Step 2: Install Dependencies
Rails comes with Net::HTTP built-in, or you can use Faraday for easier HTTP calls. We'll use Net::HTTP for this guide (no extra gems needed):
# Gemfile
gem 'httparty' # Optional: simpler HTTP calls
gem 'rack-attack' # For rate limiting
bundle install
Step 3: Create a Bot Detection Service
Create a reusable Rails service to verify requests with Device.AI:
# app/services/device_ai_service.rb
class DeviceAIService
VERIFY_URL = 'https://api.device.ai/v1/verify'.freeze
DEFAULT_TIMEOUT = 2 # seconds
def self.verify(request, threshold: 0.5)
new.verify(request, threshold: threshold)
end
def initialize
@api_key = Rails.application.credentials.dig(:device_ai_api_key) || ENV['DEVICE_AI_API_KEY']
end
def verify(request, threshold: 0.5)
return { score: 0.7, is_bot: false, error: nil } unless @api_key
begin
payload = {
ip: request.remote_ip,
user_agent: request.user_agent,
headers: extract_headers(request),
}
response = make_request(payload)
if response.is_a?(Hash) && response['score']
score = response['score'].to_f
{
score: score,
confidence: response['confidence']&.to_f || 0,
is_bot: score < threshold,
error: nil,
}
else
# API error: fail open
{ score: 0.7, is_bot: false, error: response['error'] || 'Unknown error' }
end
rescue StandardError => e
# Network error or timeout: fail open
Rails.logger.error("DeviceAI verification error: #{e.message}")
{ score: 0.7, is_bot: false, error: e.message }
end
end
private
def make_request(payload)
uri = URI(VERIFY_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = DEFAULT_TIMEOUT
request = Net::HTTP::Post.new(uri.path)
request['Content-Type'] = 'application/json'
request['Authorization'] = "Bearer #{@api_key}"
request.body = payload.to_json
response = http.request(request)
JSON.parse(response.body) if response.body
rescue Timeout::Error, Errno::ECONNREFUSED => e
# Timeout: fail open
{ 'score' => 0.7, 'error' => 'Timeout' }
end
def extract_headers(request)
{
'Accept-Language' => request.headers['HTTP_ACCEPT_LANGUAGE'],
'Accept-Encoding' => request.headers['HTTP_ACCEPT_ENCODING'],
'Referer' => request.referer,
}
end
end
Step 4: Create a Rack Middleware for Global Bot Detection
Create Rack middleware to verify all requests:
# app/middleware/bot_detection_middleware.rb
class BotDetectionMiddleware
def initialize(app, options = {})
@app = app
@threshold = options[:threshold] || 0.5
@skip_paths = options[:skip_paths] || ['/health']
end
def call(env)
request = Rack::Request.new(env)
# Skip verification for certain paths
if @skip_paths.any? { |path| request.path.start_with?(path) }
return @app.call(env)
end
# Verify device
result = DeviceAIService.verify(request, threshold: @threshold)
# Store result in request for use in controllers
env['device_ai'] = result
# Block if bot detected
if result[:is_bot]
return [
403,
{ 'Content-Type' => 'application/json' },
[{ error: 'Bot detected. Request blocked.', score: result[:score] }.to_json],
]
end
@app.call(env)
end
end
Register it in your Rails config:
# config/application.rb
class Application < Rails::Application
config.middleware.use BotDetectionMiddleware, threshold: 0.5, skip_paths: %w[/health /status]
end
Step 5: Create a before_action Filter for Controller-Level Protection
For more fine-grained control, use a Rails filter in your controllers:
# app/controllers/concerns/bot_detectable.rb
module BotDetectable
extend ActiveSupport::Concern
included do
helper_method :device_ai_result
end
def verify_device_ai(threshold: 0.5)
result = DeviceAIService.verify(request, threshold: threshold)
@device_ai_result = result
if result[:is_bot]
render json: { error: 'Bot detected' }, status: :forbidden
end
end
def device_ai_result
@device_ai_result ||= {}
end
end
Use it in your controllers:
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
include BotDetectable
before_action :verify_device_ai, only: %i[create]
def create
# Only bots with score < 0.5 are blocked
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: 'Logged in successfully'
else
redirect_to login_path, alert: 'Invalid credentials'
end
end
end
Step 6: Option 2 — Use Rack::Attack for Rate Limiting
Combine device.ai bot detection with Rack::Attack for rate limiting:
# config/initializers/rack_attack.rb
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
# Throttle requests to 10 per minute per IP
Rack::Attack.throttle('requests by ip', limit: 10, period: 60) do |req|
req.remote_ip
end
# Block obvious bots by User-Agent
Rack::Attack.blocklist('obvious bots') do |req|
# Block curl, scrapers, headless browsers
req.user_agent&.match?(/curl|scrapy|wget|python|java|PhantomJS|Headless/i)
end
# Also use device.ai for intelligent bot detection
Rack::Attack.blocklist('device ai bots') do |req|
result = DeviceAIService.verify(req, threshold: 0.5)
result[:is_bot]
end
Add to your Rails config to use Rack::Attack middleware:
# config/application.rb
config.middleware.use Rack::Attack
Step 7: Handle Bot Scores with Adaptive Thresholds
Different endpoints need different thresholds. Create specialized filters:
# app/controllers/api/v1/auth_controller.rb
class API::V1::AuthController < ApplicationController
include BotDetectable
# Strict threshold for login (prevent credential stuffing)
before_action { verify_device_ai(threshold: 0.6) }, only: %i[login]
# Standard threshold for signup
before_action { verify_device_ai(threshold: 0.5) }, only: %i[register]
# Loose threshold for comments (allow more humans with uncertainty)
before_action { verify_device_ai(threshold: 0.3) }, only: %i[post_comment]
def login
# Process login
end
def register
# Process signup
end
def post_comment
# Process comment
end
end
Step 8: Score Interpretation Table
| Score Range | Classification | Recommended Action |
|---|---|---|
| 0.0 - 0.3 | High confidence bot | Block immediately |
| 0.3 - 0.6 | Uncertain / Suspicious | Challenge (CAPTCHA, email verification) |
| 0.6 - 1.0 | Likely human | Allow request |
Adjust thresholds based on your use case:
- Login/Authentication: Use 0.6-0.7 threshold to prevent credential stuffing
- Signup/Registration: Use 0.5 threshold to balance security and user experience
- Comments/Reviews: Use 0.2-0.3 threshold to avoid false positives on UGC
- Checkout/Payment: Use 0.7 threshold to prevent payment fraud
- API Endpoints: Use 0.5 threshold by default
Step 9: Production Best Practices
9.1 Caching Scores
Cache verification results per IP address to reduce API calls and latency:
# app/services/device_ai_service.rb (updated)
class DeviceAIService
CACHE_TTL = 5.minutes
def verify(request, threshold: 0.5)
cache_key = "device_ai:#{request.remote_ip}"
cached_result = Rails.cache.read(cache_key)
return cached_result if cached_result.present?
result = fetch_verification(request, threshold: threshold)
Rails.cache.write(cache_key, result, expires_in: CACHE_TTL)
result
end
def fetch_verification(request, threshold: 0.5)
# Original verification logic
end
end
9.2 Monitoring and Logging
Track bot detection metrics for visibility:
# app/services/device_ai_service.rb (updated)
class DeviceAIService
def verify(request, threshold: 0.5)
result = fetch_verification(request, threshold: threshold)
# Log metrics
Rails.logger.info("DeviceAI: score=#{result[:score]}, bot=#{result[:is_bot]}, ip=#{request.remote_ip}")
# Send to monitoring (e.g., StatsD, Datadog)
if defined?(StatsD)
StatsD.gauge('device_ai.score', result[:score])
StatsD.increment('device_ai.bot_detected') if result[:is_bot]
end
result
end
end
9.3 Timeout and Fail-Open Strategy
Bot detection should never break your app. Always fail open:
# Always allow if verification fails
begin
result = DeviceAIService.verify(request, threshold: @threshold)
return forbidden_response if result[:is_bot]
rescue StandardError => e
# Log error but don't block user
Rails.logger.error("Bot detection failed: #{e.message}")
# Continue with request
end
9.4 Handling Rate Limit Quota
When you hit the free tier limit (1,000 verifications/day), Device.AI returns HTTP 429. Implement fallback logic:
def make_request(payload)
# ... existing code ...
response = http.request(request)
if response.code == '429'
# Quota exceeded: use stricter local rate limiting
Rails.logger.warn('DeviceAI quota exceeded')
{ 'score' => 0.5, 'error' => 'Quota exceeded' }
else
JSON.parse(response.body)
end
end
Step 10: Combining Approaches: A Production Rails Strategy
Here's a production-ready architecture combining all approaches:
# config/application.rb
class Application < Rails::Application
# 1. Global Rack middleware for coarse-grained protection
config.middleware.use BotDetectionMiddleware,
threshold: 0.5,
skip_paths: %w[/health /metrics]
# 2. Rack::Attack for rate limiting
config.middleware.use Rack::Attack
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
include BotDetectable
# 3. Controller filter for fine-grained protection
before_action :verify_device_ai, only: %i[create]
before_action { verify_device_ai(threshold: 0.6) }, only: %i[create]
def create
# Access bot detection result
score = device_ai_result[:score]
confidence = device_ai_result[:confidence]
if score < 0.3
# Definite bot: block
return render json: { error: 'Request blocked' }, status: :forbidden
elsif score < 0.6
# Uncertain: challenge with CAPTCHA
session[:require_captcha] = true
return render 'captcha_challenge'
end
# Likely human: process login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path
else
redirect_to login_path, alert: 'Invalid credentials'
end
end
end
Step 11: Testing Bot Detection
Test your bot detection implementation:
# test/services/device_ai_service_test.rb
require 'test_helper'
class DeviceAIServiceTest < ActiveSupport::TestCase
test 'identifies obvious bots' do
request = mock_request(user_agent: 'curl/7.68.0')
result = DeviceAIService.verify(request, threshold: 0.5)
assert result[:is_bot]
end
test 'allows normal browsers' do
request = mock_request(user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
result = DeviceAIService.verify(request, threshold: 0.5)
assert_not result[:is_bot]
end
private
def mock_request(user_agent: '', ip: '192.168.1.1')
request = Struct.new(:user_agent, :remote_ip, :referer, :headers).new
request.user_agent = user_agent
request.remote_ip = ip
request.referer = 'https://example.com'
request.headers = {}
request
end
end
Step 12: Handling False Positives
If legitimate users are being blocked, adjust your threshold or create an allowlist:
# app/middleware/bot_detection_middleware.rb (updated)
class BotDetectionMiddleware
ALLOWLIST_IPS = [
'192.168.0.0/16', # Corporate network
'10.0.0.0/8', # Internal network
].freeze
def should_skip_verification?(request)
# Skip for allowlisted IPs
ALLOWLIST_IPS.any? { |range| ip_in_range?(request.remote_ip, range) }
end
private
def ip_in_range?(ip, range_str)
IPAddr.new(range_str).include?(IPAddr.new(ip))
rescue StandardError
false
end
end
Pricing and Free Tier
| Plan | Daily Verifications | Price | Best For |
|---|---|---|---|
| Free | 1,000 | $0 | Development, testing, small apps |
| Pay-as-you-go | Unlimited | $0.001 per verification | Production, scaling |
| Enterprise | Custom | Custom pricing | High volume, SLAs, priority support |
For 1 million daily verifications, pay-as-you-go costs ~$300/month. Upgrade anytime at device.ai/upgrade.
Frequently Asked Questions
Does This Work with Rails 7.0+?
Yes. The code examples use standard Rails patterns (services, concerns, middleware) that work with Rails 6.0+. Adjust gem versions as needed for your Rails version.
Can I Use This with Devise?
Yes. Add the verify_device_ai filter to your Devise controller or create a custom warden strategy:
# config/initializers/devise.rb
warden_manager.after_authentication do |user, auth, opts|
result = DeviceAIService.verify(auth.request, threshold: 0.5)
throw :warden, message: :bot_detected if result[:is_bot]
end
Does This Work with Rails API?
Yes. The middleware and service work the same way for Rails API apps. JSON responses are returned automatically.
Can I Integrate with Sentry or Rollbar?
Yes. Capture bot detection errors in your error tracking service:
rescue StandardError => e
Sentry.capture_exception(e, tags: { feature: 'bot_detection' })
Rails.logger.error("DeviceAI error: #{e.message}")
end
What's the Performance Impact?
Typical verification latency is 50-150ms. With caching enabled (5 minute TTL), cache hits are <1ms. Total request overhead is negligible.
Common Integration Patterns
Pattern 1: Global Middleware + Cache
# Verify once per IP per 5 minutes
config.middleware.use BotDetectionMiddleware, threshold: 0.5
Pattern 2: API Endpoint Protection
# app/controllers/api/posts_controller.rb
class API::PostsController < ApplicationController
include BotDetectable
before_action { verify_device_ai(threshold: 0.5) }, except: %i[index show]
end
Pattern 3: Selective Blocking + Logging
result = DeviceAIService.verify(request, threshold: 0.5)
if result[:score] < 0.3
# Definite bot: block
render json: { error: 'Blocked' }, status: :forbidden
elsif result[:score] < 0.6
# Uncertain: log and allow (with optional CAPTCHA challenge)
Rails.logger.warn("Suspicious request: score=#{result[:score]}, ip=#{request.remote_ip}")
end
Next Steps
- Get a free API key at device.ai (1 minute, no signup)
- Copy the DeviceAIService into your Rails project
- Add the Rack middleware or controller filters
- Test with curl and Puppeteer
- Monitor bot detection metrics in production
- Adjust thresholds based on your false positive rate
- Upgrade to paid plan when you exceed 1,000 daily verifications
Resources
- Device.AI API Documentation — Full API reference and integration guides
- Bot Detection for Express.js — Framework Guide #4 for Node.js
- Bot Detection for Python — Framework Guide #3 for Flask and FastAPI
- Bot Detection for React — Framework Guide #2 for React apps
- Bot Detection for Next.js — Framework Guide #1 for Next.js apps
Questions? Contact support@device.ai.