Benchmarks
Table of contents
CPU-bound
1 route
100 routes
Configuration:
- 1 thread/fiber
- 100 routes
Note
Requests Per Second: This CPU-bound metric is a relatively small slice of time in comparison to the typical IO-bound tasks of web applications, such as waiting for the database.
| Framework + Server | Req/s | Mean latency | p50 | p90 | p99 | Failed requests |
|---|---|---|---|---|---|---|
| Raindeer + LowLoop | 17134.66 | 2.918 ms | 3 ms | 3 ms | 4 ms | 0 |
| Raindeer + Falcon | 14499.04 | 3.449 ms | 3 ms | 4 ms | 8 ms | 0 |
| Roda + Falcon | 11469.40 | 4.359 ms | 4 ms | 5 ms | 5 ms | 0 |
| Hanami + Falcon | 6619.51 | 7.553 ms | 7 ms | 8 ms | 9 ms | 0 |
| Sinatra + Falcon | 5691.24 | 8.785 ms | 9 ms | 9 ms | 10 ms | 0 |
| Rails + Falcon | 2709.77 | 18.452 ms | 18 ms | 20 ms | 22 ms | 0 |
*LowLoop is the default server for Raindeer
See: raindeer-benchmarks repo
IO-bound
Routing becomes a much less share of the time when it comes to IO, so we'll just benchmark with many routes.
100 routes
Integrations
These integrations are experimental and don't suport the full Raindeer API such as Interval yet.
Falcon
Iodine
If you really need CPU-bound speed then you can boot Raindeer via Iodine. Currently this increases requests per second by ~10,000 and decreases latency by ~1ms. It involves a Rack adapter (for now) and of course, the absence of LowLoop with its Matrix, Interval and Async related features.
Iodine Boot:
require 'iodine'
require_relative 'rack_adapter'
Iodine.threads = 1
Iodine.workers = 1
port = ENV.fetch('PORT', '4133')
address = ENV.fetch('BIND', '127.0.0.1')
Iodine.listen(service: :http, handler: RackAdapter.new, port:, address:)
Iodine.start
Rack Adapter:
require 'bundler/setup'
require 'rack'
require 'raindeer/boot'
Low::Events::RequestEvent.define do |observers|
observers << Providers['rain.router']
end
class RackAdapter
HEADER_NAME_CACHE = Hash.new { |cache, key| cache[key] = key.delete_prefix('HTTP_').tr('_', '-').downcase.freeze }
def call(env)
http_request = build_request(env)
response = Low::Events::RequestEvent.take(request: http_request).response
build_rack_response(response)
end
private
BODY_METHODS = %w[POST PUT PATCH].freeze
def build_request(env)
body = env['rack.input']&.read if BODY_METHODS.include?(env['REQUEST_METHOD'])
body = nil if body.nil? || body.empty?
headers = Protocol::HTTP::Headers.new(
env.filter_map do |key, value|
next unless key.start_with?('HTTP_')
[HEADER_NAME_CACHE[key], value]
end
)
query = env['QUERY_STRING']
path = query.nil? || query.empty? ? env['PATH_INFO'] : "#{env['PATH_INFO']}?#{query}"
authority = env['HTTP_HOST'] || "#{env['SERVER_NAME']}:#{env['SERVER_PORT']}"
Protocol::HTTP::Request.new(
env['rack.url_scheme'],
authority,
env['REQUEST_METHOD'],
path,
'HTTP/1.1',
headers,
body
)
end
def build_rack_response(response)
body_data = response.body.respond_to?(:file) ? File.binread(response.body.file.path) : (response.body.read || '')
rack_headers = {}
response.headers.fields.each { |key, value| rack_headers[key.to_s] = value.to_s }
rack_headers['content-length'] ||= body_data.bytesize.to_s
[response.status, rack_headers, [body_data]]
end
end