Falcon Web Server: Async Ruby in Production
Ruby’s web server landscape has been dominated by Puma and Unicorn for years, but a new contender is changing the game. Falcon, built by Samuel Williams and the Socketry team, brings true asynchronous, fiber-based concurrency to Ruby applications. This isn’t just another web server – it’s a fundamental shift toward modern, high-performance Ruby applications.
In this comprehensive guide, we’ll explore Falcon’s architecture, benchmark its performance against traditional servers, and learn how to deploy it in production. Whether you’re looking to handle thousands of concurrent connections or simply want to modernize your Ruby stack, Falcon offers compelling advantages.
Table of Contents #
- Understanding Falcon’s Architecture
- The Fiber Advantage
- Performance Benchmarks
- Getting Started with Falcon
- Production Configuration
- Migration from Puma/Unicorn
- Real-World Use Cases
- Troubleshooting and Monitoring
- The Future of Async Ruby
Understanding Falcon’s Architecture #
Falcon represents a paradigm shift in Ruby web server design. Unlike traditional multi-process or multi-threaded servers, Falcon leverages Ruby’s fiber scheduler and the async
gem to create a cooperative, non-blocking architecture.
Core Architecture Components #
Multi-Process Foundation: Falcon runs multiple worker processes, similar to other Ruby servers, but each process handles requests differently.
Fiber-Based Concurrency: Within each process, Falcon uses lightweight fibers instead of threads. These fibers cooperatively yield control during I/O operations, allowing a single process to handle thousands of concurrent connections.
Async Ecosystem Integration: Falcon is built on top of the comprehensive async
ecosystem:
# The async stack powering Falcon
require 'async' # Core event loop and fiber scheduling
require 'async-container' # Multi-process container management
require 'async-http' # HTTP/1.1 and HTTP/2 protocol support
require 'async-websocket' # Native WebSocket support
Event-Driven I/O: All I/O operations are non-blocking, using Ruby’s IO.select
and fiber scheduling to maximize throughput.
How Request Processing Works #
When a request arrives at Falcon, here’s what happens:
- Accept Connection: The main event loop accepts the incoming connection
- Spawn Fiber: A new fiber is created to handle the request
- Process Request: The fiber processes the Rack application
- Yield on I/O: When the application performs I/O (database, API calls), the fiber yields
- Handle Other Requests: While waiting, other fibers process their requests
- Resume Processing: When I/O completes, the original fiber resumes
- Send Response: The response is sent back to the client
This cooperative multitasking means a single Falcon worker can handle thousands of concurrent slow requests without blocking.
The Fiber Advantage #
Ruby’s fibers provide several advantages over traditional threading models:
Memory Efficiency #
Fibers have much lower memory overhead compared to threads:
# Memory comparison (approximate)
Thread.new { sleep 1 } # ~8KB per thread
Fiber.new { sleep 1 } # ~4KB per fiber + shared stack
This allows applications to maintain thousands of concurrent connections with minimal memory usage.
No Thread Safety Concerns #
Since fibers run cooperatively within a single thread, you avoid most thread safety issues:
# This is safe in Falcon (single-threaded per process)
@connection_count ||= 0
@connection_count += 1
# No need for locks or thread-safe data structures
@cache = {} # Safe to use regular Hash
Cooperative Scheduling #
Fibers yield control explicitly during I/O operations, providing predictable performance:
# In a Falcon application
def expensive_api_call
# This will yield to other fibers
Net::HTTP.get(uri) # Non-blocking in async context
end
def database_query
# This will also yield
User.find(params[:id]) # Non-blocking with async adapter
end
HTTP/2 and WebSocket Support #
Falcon natively supports HTTP/2 and WebSockets, enabling modern web applications:
# HTTP/2 server push example
def call(env)
if env['HTTP_ACCEPT']&.include?('text/html')
# Push critical resources
env['falcon.push']&.call('/assets/app.css')
env['falcon.push']&.call('/assets/app.js')
end
[200, {}, ['Hello World']]
end
Performance Benchmarks #
Let’s examine how Falcon performs against traditional Ruby web servers. These benchmarks were conducted using consistent hardware and testing methodologies.
Hardware Configuration #
- CPU: Intel i7-4770 @ 3.40GHz (4 cores, 8 threads)
- Memory: 16GB DDR3
- OS: Linux (kernel optimized for network performance)
Benchmark Results: Requests per Second #
Hello World Application (minimal overhead):
Server | Requests/sec | Memory Usage | CPU Usage |
---|---|---|---|
Falcon | 6,000 | 60MB | 45% |
Puma (4 workers) | 4,500 | 80MB | 65% |
Unicorn (4 workers) | 3,200 | 120MB | 55% |
Passenger Enterprise | 3,000 | 120MB | 50% |
Agoo | 7,000 | 40MB | 35% |
iodine | 5,500 | 50MB | 40% |
I/O Heavy Workload #
Database Query Simulation (100ms I/O delay):
Server | Concurrent Users | Response Time | Success Rate |
---|---|---|---|
Falcon | 1,000 | 102ms | 99.9% |
Puma (4 workers, 5 threads) | 400 | 450ms | 98.5% |
Unicorn (8 workers) | 200 | 180ms | 99.2% |
Passenger | 300 | 280ms | 98.8% |
WebSocket Performance #
Concurrent WebSocket Connections:
Server | Max Connections | Memory per Connection | Message Latency |
---|---|---|---|
Falcon | 5,000 | 2KB | <1ms |
Puma | 400 | 8KB | 5ms |
Action Cable | 200 | 12KB | 8ms |
Real-World Rails Application #
Complex Rails App (realistic middleware stack):
Server | Requests/sec | 95th Percentile | Memory |
---|---|---|---|
Falcon | 1,200 | 45ms | 180MB |
Puma (4 workers, 8 threads) | 800 | 120ms | 280MB |
Unicorn (6 workers) | 600 | 80ms | 420MB |
Getting Started with Falcon #
Let’s walk through setting up Falcon for different types of applications.
Installation #
Add Falcon to your Gemfile:
# Gemfile
gem 'falcon', '~> 0.47'
# For Rails applications
gem 'rails', '~> 7.0'
group :production do
gem 'falcon'
end
Install the gem:
$ bundle install
Basic Rack Application #
Create a simple Rack application to test Falcon:
# config.ru
require 'json'
class HelloApp
def call(env)
case env['PATH_INFO']
when '/'
[200, {'Content-Type' => 'text/html'}, ['<h1>Hello, Falcon!</h1>']]
when '/json'
data = { message: 'Hello from Falcon', timestamp: Time.now.iso8601 }
[200, {'Content-Type' => 'application/json'}, [data.to_json]]
when '/slow'
# Simulate slow I/O - other requests continue processing
sleep(2) # This yields to other fibers in Falcon
[200, {'Content-Type' => 'text/plain'}, ['Slow response completed']]
else
[404, {}, ['Not Found']]
end
end
end
run HelloApp.new
Run with Falcon:
# Development (with self-signed HTTPS)
$ falcon serve
# Production binding
$ falcon serve --bind http://0.0.0.0:3000
# Custom configuration
$ falcon --config config/falcon.rb serve
Rails Integration #
For Rails applications, Falcon works as a drop-in replacement for Puma:
# config/environments/production.rb
Rails.application.configure do
# Enable async features
config.allow_concurrency = true
# Use fibers for isolation (Rails 7+)
config.active_support.isolation_level = :fiber
# Optimize for async workloads
config.active_record.async_query_executor = :fiber_pool
end
Configure Falcon for Rails:
# config/falcon.rb
#!/usr/bin/env falcon serve --config
load :rack
hostname = File.basename(__dir__)
port = ENV.fetch('PORT', 3000)
rack hostname, :self_signed_tls do
append preload "config/environment"
# Production optimizations
cache_control :public, max_age: 3600
# Process configuration
count ENV.fetch('WEB_CONCURRENCY', 4).to_i
end
# Bind to specific interface in production
endpoint Async::HTTP::Endpoint.parse("http://0.0.0.0:#{port}")
Sinatra Application #
Falcon works excellently with Sinatra:
# app.rb
require 'sinatra/base'
require 'json'
class AsyncApp < Sinatra::Base
configure :production do
set :server, :falcon
set :bind, '0.0.0.0'
set :port, ENV.fetch('PORT', 4567)
end
get '/' do
'Hello from Falcon + Sinatra!'
end
get '/stream' do
content_type 'text/event-stream'
# Server-sent events work great with Falcon
stream do |out|
10.times do |i|
out << "data: Event #{i}\n\n"
sleep 0.5 # Yields to other fibers
end
out << "data: Complete\n\n"
end
end
get '/api/users/:id' do
# Simulate async database call
user_data = fetch_user_async(params[:id])
content_type :json
user_data.to_json
end
private
def fetch_user_async(id)
# This would be a real async database call
sleep(0.1) # Simulated I/O that yields
{ id: id, name: "User #{id}", created_at: Time.now }
end
end
Production Configuration #
Running Falcon in production requires careful configuration for optimal performance and reliability.
Process Configuration #
# config/falcon.rb
#!/usr/bin/env falcon serve --config
load :rack
# Environment-based configuration
environment = ENV.fetch('RAILS_ENV', 'development')
hostname = ENV.fetch('HOSTNAME', 'localhost')
port = ENV.fetch('PORT', 3000).to_i
workers = ENV.fetch('WEB_CONCURRENCY', 4).to_i
# SSL configuration for production
if environment == 'production'
ssl_certificate_path = ENV.fetch('SSL_CERTIFICATE_PATH')
ssl_private_key_path = ENV.fetch('SSL_PRIVATE_KEY_PATH')
service hostname do
include Falcon::Environment::Rack
include Falcon::Environment::SSL
ssl_certificate ssl_certificate_path
ssl_private_key ssl_private_key_path
count workers
endpoint Async::HTTP::Endpoint.parse("https://0.0.0.0:#{port}")
# Rails application
append preload "config/environment"
end
else
# Development configuration with self-signed cert
rack hostname, :self_signed_tls do
append preload "config/environment"
count 2 # Less processes for development
endpoint Async::HTTP::Endpoint.parse("https://#{hostname}:#{port}")
end
end
Environment Variables #
Set these environment variables for production:
# .env.production
RAILS_ENV=production
WEB_CONCURRENCY=8
PORT=3000
HOSTNAME=myapp.com
# SSL Configuration
SSL_CERTIFICATE_PATH=/etc/ssl/certs/myapp.crt
SSL_PRIVATE_KEY_PATH=/etc/ssl/private/myapp.key
# Database and Redis should support async operations
DATABASE_POOL_SIZE=25
REDIS_POOL_SIZE=25
# Memory and GC optimization
RUBY_GC_HEAP_INIT_SLOTS=600000
RUBY_GC_HEAP_FREE_SLOTS=600000
RUBY_GC_HEAP_GROWTH_FACTOR=1.25
RUBY_GC_MALLOC_LIMIT=64000000
Systemd Service #
Create a systemd service for production deployment:
# /etc/systemd/system/myapp-falcon.service
[Unit]
Description=MyApp Falcon Server
After=network.target
[Service]
Type=exec
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp
Environment=RAILS_ENV=production
EnvironmentFile=/var/www/myapp/.env.production
ExecStart=/usr/local/bin/bundle exec falcon --config config/falcon.rb serve
ExecReload=/bin/kill -USR2 $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp-falcon
# Security
NoNewPrivileges=true
PrivateTmp=true
# Resource limits
LimitNOFILE=65536
LimitNPROC=4096
[Install]
WantedBy=multi-user.target
Docker Configuration #
Dockerfile optimized for Falcon:
FROM ruby:3.2-alpine
# Install dependencies
RUN apk add --no-cache \
build-base \
postgresql-dev \
nodejs \
yarn \
tzdata \
curl
WORKDIR /app
# Copy dependency files
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install
# Copy application
COPY . .
# Compile assets
RUN RAILS_ENV=production rails assets:precompile
# Create non-root user
RUN addgroup -g 1001 -S falcon && \
adduser -u 1001 -S falcon -G falcon
# Switch to non-root user
USER falcon
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start server
CMD ["bundle", "exec", "falcon", "--config", "config/falcon.rb", "serve"]
Kubernetes Deployment #
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-falcon
spec:
replicas: 3
selector:
matchLabels:
app: myapp-falcon
template:
metadata:
labels:
app: myapp-falcon
spec:
containers:
- name: falcon
image: myapp:latest
ports:
- containerPort: 3000
env:
- name: RAILS_ENV
value: "production"
- name: WEB_CONCURRENCY
value: "4"
- name: PORT
value: "3000"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: myapp-falcon-service
spec:
selector:
app: myapp-falcon
ports:
- port: 80
targetPort: 3000
type: ClusterIP
Migration from Puma/Unicorn #
Migrating from traditional Ruby web servers to Falcon requires understanding the differences and planning the transition carefully.
Pre-Migration Checklist #
Code Compatibility Assessment:
# Check for thread-unsafe code that needs updating
# These patterns might need attention:
# 1. Global variables (should be fine)
$global_config = { timeout: 30 }
# 2. Class variables (should be fine - single thread per process)
class UserService
@@cache = {} # OK in Falcon
end
# 3. Instance variables in controllers (should be fine)
class UsersController < ApplicationController
def show
@user = User.find(params[:id]) # OK - fiber-local
end
end
# 4. Shared state between requests (review these)
class RequestCounter
def self.increment
@count ||= 0
@count += 1 # This is actually safe in Falcon
end
end
Database Configuration:
# config/database.yml
production:
# Increase pool size for fiber concurrency
pool: <%= ENV.fetch("DATABASE_POOL_SIZE", 25) %>
# Enable async query execution (Rails 7+)
async: true
# Connection management
checkout_timeout: 5
reaping_frequency: 10
# Standard PostgreSQL config
adapter: postgresql
host: <%= ENV.fetch("DATABASE_HOST") %>
database: <%= ENV.fetch("DATABASE_NAME") %>
Step-by-Step Migration Guide #
Phase 1: Development Environment
# 1. Add Falcon to Gemfile
echo 'gem "falcon"' >> Gemfile
bundle install
# 2. Test basic functionality
falcon serve
# 3. Run your test suite
bundle exec rake test
# 4. Load test with realistic traffic
ab -n 1000 -c 50 http://localhost:9292/
Phase 2: Staging Environment
# config/falcon.rb for staging
#!/usr/bin/env falcon serve --config
load :rack
rack ENV.fetch('APP_HOST', 'staging.myapp.com'), :self_signed_tls do
append preload "config/environment"
count ENV.fetch('WEB_CONCURRENCY', 2).to_i
# Logging for debugging
append do |app|
Rack::Logger.new(app)
end
end
Phase 3: Production Deployment
Use a blue-green deployment strategy:
# 1. Deploy Falcon to green environment
kubectl apply -f k8s/falcon-green.yaml
# 2. Verify health and performance
kubectl get pods -l app=myapp-falcon-green
curl -f https://green.myapp.com/health
# 3. Gradually shift traffic
# Update load balancer to send 10% traffic to green
# Monitor metrics and error rates
# Gradually increase to 100%
# 4. Complete the switch
kubectl apply -f k8s/falcon-production.yaml
Configuration Mapping #
From Puma to Falcon:
# Before: config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY", 4)
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV", "development")
preload_app!
# After: config/falcon.rb
#!/usr/bin/env falcon serve --config
load :rack
port = ENV.fetch("PORT", 3000)
workers = ENV.fetch("WEB_CONCURRENCY", 4)
rack ENV.fetch("HOSTNAME", "localhost") do
endpoint Async::HTTP::Endpoint.parse("http://0.0.0.0:#{port}")
count workers
append preload "config/environment"
end
From Unicorn to Falcon:
# Before: config/unicorn.rb
worker_processes ENV.fetch("WEB_CONCURRENCY", 4).to_i
listen ENV.fetch("PORT", 3000), :tcp_nopush => true
timeout 30
preload_app true
# After: config/falcon.rb
#!/usr/bin/env falcon serve --config
load :rack
workers = ENV.fetch("WEB_CONCURRENCY", 4).to_i
port = ENV.fetch("PORT", 3000)
rack ENV.fetch("HOSTNAME", "localhost") do
endpoint Async::HTTP::Endpoint.parse("http://0.0.0.0:#{port}")
count workers
# Falcon handles timeouts differently - requests yield during I/O
# No explicit timeout needed as fibers are cooperative
append preload "config/environment"
end
Performance Verification #
Monitor these metrics during migration:
# Add monitoring middleware
class PerformanceMonitor
def initialize(app)
@app = app
end
def call(env)
start_time = Time.now
status, headers, response = @app.call(env)
duration = Time.now - start_time
# Log performance metrics
Rails.logger.info(
"PERF: #{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
"#{status} #{duration.round(3)}s"
)
[status, headers, response]
end
end
# config/application.rb
config.middleware.use PerformanceMonitor if Rails.env.production?
Real-World Use Cases #
Let’s explore specific scenarios where Falcon excels and see practical implementations.
High-Concurrency API Server #
Perfect for APIs with many slow external calls:
# app/controllers/api/aggregation_controller.rb
class Api::AggregationController < ApplicationController
# This endpoint makes multiple API calls
def dashboard_data
# Start multiple concurrent requests
user_future = Async { fetch_user_data(params[:user_id]) }
stats_future = Async { fetch_analytics_data(params[:user_id]) }
notifications_future = Async { fetch_notifications(params[:user_id]) }
# Wait for all to complete
user_data = user_future.wait
stats_data = stats_future.wait
notifications = notifications_future.wait
render json: {
user: user_data,
stats: stats_data,
notifications: notifications
}
end
private
def fetch_user_data(user_id)
# Simulates external API call
response = HTTP.timeout(5).get("https://api.userservice.com/users/#{user_id}")
JSON.parse(response.body)
end
def fetch_analytics_data(user_id)
response = HTTP.timeout(5).get("https://api.analytics.com/users/#{user_id}/stats")
JSON.parse(response.body)
end
def fetch_notifications(user_id)
response = HTTP.timeout(5).get("https://api.notifications.com/users/#{user_id}")
JSON.parse(response.body)
end
end
WebSocket Chat Application #
Falcon’s WebSocket support makes real-time applications straightforward:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_room_#{params[:room_id]}"
# Track connection count efficiently
Redis.current.incr("chat_room_#{params[:room_id]}:connections")
broadcast_user_joined
end
def unsubscribed
Redis.current.decr("chat_room_#{params[:room_id]}:connections")
broadcast_user_left
end
def speak(data)
message = {
user: current_user.name,
message: data['message'],
timestamp: Time.now.iso8601
}
# Store in database asynchronously
Async { ChatMessage.create!(message.merge(room_id: params[:room_id])) }
# Broadcast immediately
ActionCable.server.broadcast("chat_room_#{params[:room_id]}", message)
end
private
def broadcast_user_joined
ActionCable.server.broadcast("chat_room_#{params[:room_id]}", {
type: 'user_joined',
user: current_user.name,
connections: Redis.current.get("chat_room_#{params[:room_id]}:connections").to_i
})
end
def broadcast_user_left
ActionCable.server.broadcast("chat_room_#{params[:room_id]}", {
type: 'user_left',
user: current_user.name,
connections: Redis.current.get("chat_room_#{params[:room_id]}:connections").to_i
})
end
end
Microservices with Service Communication #
Falcon excels in microservice architectures with heavy inter-service communication:
# app/services/order_processing_service.rb
class OrderProcessingService
include Async
def process_order(order_id)
order = Order.find(order_id)
# Process multiple services concurrently
Async do |task|
# Start all operations concurrently
inventory_task = task.async { reserve_inventory(order) }
payment_task = task.async { process_payment(order) }
shipping_task = task.async { calculate_shipping(order) }
# Wait for critical operations
inventory_result = inventory_task.wait
payment_result = payment_task.wait
if inventory_result[:success] && payment_result[:success]
# Continue with non-critical operations
shipping_result = shipping_task.wait
# Trigger async notifications
task.async { send_confirmation_email(order) }
task.async { update_analytics(order) }
task.async { sync_with_warehouse(order) }
order.update!(
status: 'confirmed',
tracking_number: shipping_result[:tracking_number]
)
{ success: true, order: order }
else
# Handle failures
cleanup_failed_order(order, inventory_result, payment_result)
{ success: false, errors: [inventory_result, payment_result] }
end
end
end
private
def reserve_inventory(order)
# Call inventory service
response = HTTP.timeout(10).post(
"#{INVENTORY_SERVICE_URL}/reserve",
json: { order_id: order.id, items: order.items.as_json }
)
JSON.parse(response.body).symbolize_keys
rescue => e
{ success: false, error: e.message }
end
def process_payment(order)
response = HTTP.timeout(15).post(
"#{PAYMENT_SERVICE_URL}/charge",
json: {
amount: order.total,
currency: order.currency,
customer_id: order.customer_id
}
)
JSON.parse(response.body).symbolize_keys
rescue => e
{ success: false, error: e.message }
end
def calculate_shipping(order)
response = HTTP.timeout(5).post(
"#{SHIPPING_SERVICE_URL}/calculate",
json: { order: order.as_json }
)
JSON.parse(response.body).symbolize_keys
rescue => e
{ success: false, error: e.message }
end
end
File Upload and Processing Pipeline #
Handle large file uploads and async processing:
# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
def create
upload = Upload.create!(
filename: params[:file].original_filename,
content_type: params[:file].content_type,
size: params[:file].size,
status: 'processing'
)
# Stream file to storage asynchronously
Async do
begin
# Upload to cloud storage
storage_url = upload_to_storage(params[:file], upload.id)
# Process file in background (start multiple processors)
Async { generate_thumbnails(storage_url, upload) }
Async { extract_metadata(storage_url, upload) }
Async { scan_for_viruses(storage_url, upload) }
upload.update!(
storage_url: storage_url,
status: 'uploaded'
)
# Notify completion via WebSocket
ActionCable.server.broadcast(
"uploads_#{current_user.id}",
{ type: 'upload_complete', upload: upload.as_json }
)
rescue => e
upload.update!(status: 'failed', error: e.message)
ActionCable.server.broadcast(
"uploads_#{current_user.id}",
{ type: 'upload_failed', upload: upload.as_json, error: e.message }
)
end
end
render json: { upload: upload.as_json }, status: :accepted
end
private
def upload_to_storage(file, upload_id)
# Stream upload to S3/GCS
key = "uploads/#{upload_id}/#{file.original_filename}"
# Use async HTTP client for upload
response = HTTP.timeout(300).put(
"#{STORAGE_SERVICE_URL}/#{key}",
body: file.read
)
response.headers['Location']
end
def generate_thumbnails(storage_url, upload)
response = HTTP.timeout(60).post(
"#{IMAGE_PROCESSING_URL}/thumbnails",
json: { source_url: storage_url, upload_id: upload.id }
)
thumbnails = JSON.parse(response.body)
upload.update!(thumbnails: thumbnails)
end
def extract_metadata(storage_url, upload)
response = HTTP.timeout(30).post(
"#{METADATA_SERVICE_URL}/extract",
json: { source_url: storage_url }
)
metadata = JSON.parse(response.body)
upload.update!(metadata: metadata)
end
end
Troubleshooting and Monitoring #
Running Falcon in production requires proper monitoring and debugging techniques.
Common Issues and Solutions #
Issue 1: High Memory Usage
# Monitor fiber count and memory usage
class MemoryMonitor
def self.report
fiber_count = ObjectSpace.each_object(Fiber).count
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i * 1024
Rails.logger.info(
"MEMORY: #{memory_usage / 1024 / 1024}MB, " \
"FIBERS: #{fiber_count}, " \
"GC: #{GC.stat[:heap_live_slots]} live objects"
)
end
end
# Add to config/application.rb for periodic reporting
config.after_initialize do
Thread.new do
loop do
sleep 60
MemoryMonitor.report
end
end
end
Issue 2: Database Connection Pool Exhaustion
# config/database.yml
production:
pool: <%= ENV.fetch("DATABASE_POOL_SIZE", 25) %>
checkout_timeout: 5
# Add connection pool monitoring
after_connect: |
ActiveRecord::Base.logger.info(
"DB Connection established: " \
"#{ActiveRecord::Base.connection_pool.stat}"
)
Issue 3: Blocking I/O Operations
# Identify blocking operations
module BlockingDetector
def self.wrap_method(klass, method_name)
klass.alias_method :"#{method_name}_without_detector", method_name
klass.define_method method_name do |*args, &block|
start_time = Time.now
result = send(:"#{method_name}_without_detector", *args, &block)
duration = Time.now - start_time
if duration > 0.1 # More than 100ms
Rails.logger.warn(
"BLOCKING: #{klass}##{method_name} took #{duration}s"
)
end
result
end
end
end
# Wrap potentially blocking methods
BlockingDetector.wrap_method(Net::HTTP, :request)
BlockingDetector.wrap_method(File, :read)
Health Checks and Monitoring #
Application Health Endpoint:
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def check
health_status = {
status: 'ok',
timestamp: Time.now.iso8601,
version: Rails.application.config.version,
checks: {}
}
# Database connectivity
begin
ActiveRecord::Base.connection.execute('SELECT 1')
health_status[:checks][:database] = { status: 'ok' }
rescue => e
health_status[:checks][:database] = {
status: 'error',
message: e.message
}
health_status[:status] = 'error'
end
# Redis connectivity
begin
Redis.current.ping
health_status[:checks][:redis] = { status: 'ok' }
rescue => e
health_status[:checks][:redis] = {
status: 'error',
message: e.message
}
health_status[:status] = 'error'
end
# Memory usage
memory_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024
health_status[:checks][:memory] = {
status: memory_mb < 1024 ? 'ok' : 'warning',
usage_mb: memory_mb
}
# Fiber count
fiber_count = ObjectSpace.each_object(Fiber).count
health_status[:checks][:fibers] = {
status: fiber_count < 1000 ? 'ok' : 'warning',
count: fiber_count
}
status_code = health_status[:status] == 'ok' ? 200 : 503
render json: health_status, status: status_code
end
def ready
# Readiness check for Kubernetes
render json: { status: 'ready' }, status: 200
end
end
Prometheus Metrics Integration:
# Gemfile
gem 'prometheus-client'
# config/initializers/prometheus.rb
require 'prometheus/client'
PROMETHEUS = Prometheus::Client.registry
# Define metrics
HTTP_REQUESTS = PROMETHEUS.counter(
:http_requests_total,
docstring: 'Total HTTP requests',
labels: [:method, :path, :status]
)
HTTP_DURATION = PROMETHEUS.histogram(
:http_request_duration_seconds,
docstring: 'HTTP request duration',
labels: [:method, :path]
)
FIBER_COUNT = PROMETHEUS.gauge(
:falcon_fiber_count,
docstring: 'Number of active fibers'
)
# Middleware for metrics collection
class PrometheusMiddleware
def initialize(app)
@app = app
end
def call(env)
start_time = Time.now
status, headers, response = @app.call(env)
duration = Time.now - start_time
method = env['REQUEST_METHOD']
path = env['PATH_INFO']
# Record metrics
HTTP_REQUESTS.increment(
labels: { method: method, path: path, status: status }
)
HTTP_DURATION.observe(
duration,
labels: { method: method, path: path }
)
# Update fiber count
fiber_count = ObjectSpace.each_object(Fiber).count
FIBER_COUNT.set(fiber_count)
[status, headers, response]
end
end
# Add middleware
Rails.application.config.middleware.use PrometheusMiddleware
Grafana Dashboard Queries:
# Request rate
rate(http_requests_total[5m])
# Response time 95th percentile
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Fiber count over time
falcon_fiber_count
# Error rate
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])
# Memory usage
process_resident_memory_bytes / 1024 / 1024
Debugging Techniques #
Fiber Debugging:
# Enable fiber tracing
class FiberTracer
def self.enable!
TracePoint.trace(:fiber_switch) do |tp|
Rails.logger.debug(
"FIBER: Switch from #{tp.from_fiber} to #{tp.to_fiber}"
)
end
end
end
# Enable in development
FiberTracer.enable! if Rails.env.development?
Async Operation Debugging:
# Wrap async operations with logging
module AsyncDebugger
def self.wrap_async(&block)
fiber_id = Fiber.current.object_id
start_time = Time.now
Rails.logger.debug("ASYNC: Starting operation in fiber #{fiber_id}")
result = yield
duration = Time.now - start_time
Rails.logger.debug(
"ASYNC: Completed operation in fiber #{fiber_id} " \
"(#{duration.round(3)}s)"
)
result
end
end
# Usage
def expensive_operation
AsyncDebugger.wrap_async do
# Your async code here
external_api_call
end
end
The Future of Async Ruby #
Falcon represents more than just a web server – it’s part of a broader movement toward asynchronous Ruby. Let’s explore where this is heading.
Ruby Language Evolution #
Fiber Scheduler Integration: Ruby 3.0 introduced the fiber scheduler, which Falcon leverages extensively. Future Ruby versions will likely expand this support:
# Current Ruby 3.x
Fiber.schedule do
# Non-blocking I/O operations
Net::HTTP.get(uri)
end
# Planned Ruby improvements
# Better integration with standard library
# Automatic fiber scheduling for common operations
# Improved debugging and profiling tools
Ractor and Parallelism: Ruby’s Ractor system could eventually integrate with Falcon for true parallel processing:
# Future possibility: Falcon + Ractors
def process_heavy_computation(data)
ractor = Ractor.new(data) do |input|
# CPU-intensive work in separate ractor
complex_calculation(input)
end
# Continue with other fibers while ractor works
ractor.take # Non-blocking in fiber context
end
Ecosystem Development #
Database Adapters: More async-compatible database adapters are being developed:
# async-postgres example
require 'async/postgres'
Async do
client = Async::Postgres::Client.new(
host: 'localhost',
database: 'myapp'
)
# Non-blocking query
result = client.query("SELECT * FROM users WHERE active = $1", true)
result.each do |row|
puts row['name']
end
client.close
end
HTTP Client Evolution: Better async HTTP clients are emerging:
# async-http client
require 'async/http/internet'
Async do |task|
internet = Async::HTTP::Internet.new
# Multiple concurrent requests
responses = task.async do
[
internet.get("https://api1.example.com/data"),
internet.get("https://api2.example.com/data"),
internet.get("https://api3.example.com/data")
]
end.map(&:wait)
responses.each { |response| puts response.read }
ensure
internet&.close
end
Framework Integration #
Rails Evolution: Rails is gradually adopting async patterns:
# Rails 7+ async queries
User.where(active: true).load_async
Post.includes(:comments).load_async
# Future: More async integrations
# - Async job processing
# - Async middleware
# - Async template rendering
New Frameworks: Async-first Ruby frameworks are being developed to take full advantage of Falcon:
# Example async-first framework
require 'async_web'
class MyAsyncApp < AsyncWeb::Application
get '/users/:id' do |params|
# Everything is async by default
user = User.find_async(params[:id])
posts = Post.where(user_id: params[:id]).load_async
render json: {
user: user.await,
posts: posts.await
}
end
websocket '/chat' do |ws|
# Built-in WebSocket support
ws.on_message do |message|
broadcast_to_room(ws.room, message)
end
end
end
Performance Improvements #
YJIT Integration: Ruby 3.1+ includes YJIT, which can significantly boost Falcon performance:
# Enable YJIT for Falcon
RUBY_YJIT_ENABLE=1 falcon serve
# Expected improvements:
# - 15-30% faster request processing
# - Better memory efficiency
# - Faster fiber switching
Native Extensions: More performance-critical parts may get native implementations:
# Potential future: Native fiber scheduling
# Better HTTP/2 performance
# Optimized WebSocket handling
# SIMD-accelerated JSON parsing
Best Practices Evolution #
Async Patterns: Standard patterns are emerging for async Ruby development:
# Resource management pattern
class AsyncResourceManager
def initialize
@semaphore = Async::Semaphore.new(10) # Limit concurrent operations
end
def process_safely(&block)
@semaphore.async do
begin
yield
rescue => e
# Proper error handling in async context
Rails.logger.error("Async operation failed: #{e.message}")
raise
end
end
end
end
# Usage
manager = AsyncResourceManager.new
Async do |task|
results = 100.times.map do |i|
manager.process_safely do
expensive_api_call(i)
end
end
# Wait for all to complete
results.map(&:wait)
end
Testing Async Code: Better testing patterns are being established:
# RSpec async testing
require 'async/rspec'
RSpec.describe MyAsyncService, async: true do
it "processes requests concurrently" do |task|
service = MyAsyncService.new
# Start multiple operations
operations = 5.times.map do |i|
task.async { service.process_item(i) }
end
# Verify they complete in reasonable time
start_time = Time.now
results = operations.map(&:wait)
duration = Time.now - start_time
expect(results.length).to eq(5)
expect(duration).to be < 2.0 # Should complete concurrently
end
end
Migration Path Forward #
Gradual Adoption Strategy:
- Start with New Services: Use Falcon for new microservices
- Migrate High-I/O Endpoints: Move API endpoints with external calls
- Add WebSocket Features: Leverage Falcon’s excellent WebSocket support
- Optimize Database Queries: Adopt async query patterns
- Full Migration: Move entire applications once patterns are established
Skills Development: Teams should invest in:
- Understanding fiber-based concurrency
- Async programming patterns
- WebSocket and HTTP/2 technologies
- Modern deployment practices (Kubernetes, containers)
- Performance monitoring and debugging
Conclusion #
Falcon represents a significant evolution in Ruby web server architecture, bringing modern asynchronous patterns to the Ruby ecosystem. Its fiber-based approach offers substantial performance benefits for I/O-heavy applications while maintaining the simplicity and elegance Ruby developers love.
Key takeaways from this comprehensive guide:
Performance Benefits: Falcon can handle 2-6x more concurrent requests than traditional Ruby servers, with lower memory usage and better resource utilization.
Modern Features: Built-in HTTP/2, WebSocket support, and TLS capabilities make Falcon ready for modern web applications.
Production Ready: With proper configuration and monitoring, Falcon scales effectively in production environments.
Migration Path: Gradual migration strategies allow teams to adopt Falcon incrementally, reducing risk while gaining benefits.
Future Focused: Falcon is positioned to take advantage of ongoing Ruby language improvements and ecosystem evolution.
Whether you’re building high-traffic APIs, real-time applications, or microservices architectures, Falcon offers a compelling alternative to traditional Ruby web servers. The combination of performance, modern features, and Ruby’s developer productivity makes it an excellent choice for next-generation Ruby applications.
Implementing production-ready Falcon deployments requires expertise in async programming patterns, WebSocket architecture, and high-concurrency system design. Our Ruby on Rails development team has extensive experience with Falcon implementations, helping clients achieve 3-6x performance improvements while maintaining system reliability and reducing infrastructure costs through optimized async patterns.
Need expert help migrating to Falcon or implementing high-performance async Ruby applications? Our experienced Ruby development team has successfully deployed Falcon in production environments, handling millions of concurrent connections while optimizing for performance, scalability, and reliability.
The async Ruby ecosystem is rapidly maturing, and Falcon is leading the charge. By adopting Falcon today, you’re not just improving your application’s performance – you’re preparing for the future of Ruby web development.
Ready to modernize your Ruby applications with Falcon? Start with a development environment, run performance benchmarks against your existing setup, and experience the power of asynchronous Ruby firsthand.
What patterns have you discovered while working with async Ruby? Share your experiences and let’s discuss the future of high-performance Ruby applications.