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 #

  1. Understanding Falcon’s Architecture
  2. The Fiber Advantage
  3. Performance Benchmarks
  4. Getting Started with Falcon
  5. Production Configuration
  6. Migration from Puma/Unicorn
  7. Real-World Use Cases
  8. Troubleshooting and Monitoring
  9. 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:

  1. Accept Connection: The main event loop accepts the incoming connection
  2. Spawn Fiber: A new fiber is created to handle the request
  3. Process Request: The fiber processes the Rack application
  4. Yield on I/O: When the application performs I/O (database, API calls), the fiber yields
  5. Handle Other Requests: While waiting, other fibers process their requests
  6. Resume Processing: When I/O completes, the original fiber resumes
  7. 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):

ServerRequests/secMemory UsageCPU Usage
Falcon6,00060MB45%
Puma (4 workers)4,50080MB65%
Unicorn (4 workers)3,200120MB55%
Passenger Enterprise3,000120MB50%
Agoo7,00040MB35%
iodine5,50050MB40%

I/O Heavy Workload #

Database Query Simulation (100ms I/O delay):

ServerConcurrent UsersResponse TimeSuccess Rate
Falcon1,000102ms99.9%
Puma (4 workers, 5 threads)400450ms98.5%
Unicorn (8 workers)200180ms99.2%
Passenger300280ms98.8%

WebSocket Performance #

Concurrent WebSocket Connections:

ServerMax ConnectionsMemory per ConnectionMessage Latency
Falcon5,0002KB<1ms
Puma4008KB5ms
Action Cable20012KB8ms

Real-World Rails Application #

Complex Rails App (realistic middleware stack):

ServerRequests/sec95th PercentileMemory
Falcon1,20045ms180MB
Puma (4 workers, 8 threads)800120ms280MB
Unicorn (6 workers)60080ms420MB

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:

  1. Start with New Services: Use Falcon for new microservices
  2. Migrate High-I/O Endpoints: Move API endpoints with external calls
  3. Add WebSocket Features: Leverage Falcon’s excellent WebSocket support
  4. Optimize Database Queries: Adopt async query patterns
  5. 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.