Sinatra vs Rails — When I Reach for the Smaller Tool
by Eric Hanson, Backend Developer at Clean Systems Consulting
The service that does not need generators
You are building a webhook receiver. It has three endpoints: one to receive events, one to replay them, one health check for your load balancer. Someone on your team is about to run rails new and you need to talk them out of it. Rails would technically work — it always does — but you would be shipping a service with an ORM, an asset pipeline skeleton, mailer stubs, and 40 megabytes of framework for a service with 200 lines of business logic. Sinatra is the right tool here.
What Sinatra is and is not
Sinatra is a DSL built on Rack. It gives you route matching, request/response handling, and helpers for rendering responses. It does not give you generators, an ORM, a standard directory layout, a testing infrastructure, or any of the other opinions Rails brings. That is the point.
# The whole application can live in one file for simple services
require 'sinatra'
require 'sinatra/json'
require 'json'
set :logging, true
post '/webhooks/events' do
payload = JSON.parse(request.body.read)
EventProcessor.new(payload).process
json status: 'accepted'
end
post '/webhooks/replay' do
event_id = params[:event_id]
event = EventStore.find(event_id)
halt 404, json(error: 'Event not found') unless event
EventProcessor.new(event.payload).process
json status: 'replayed'
end
get '/health' do
json status: 'ok', version: ENV['APP_VERSION']
end
That is a deployable Sinatra application. No MVC, no config/ directory tree, no Gemfile with 60 transitive dependencies. Boot time is under a second. Memory footprint is around 30-50MB depending on what you pull in.
The cases where Sinatra earns its place
Internal APIs and service adapters: A service whose only job is to translate between two external API formats, validate payloads, and forward them. There is no domain model. There is no persistence layer. Rails would be waste; Sinatra is exactly right.
Lambda and edge function targets: When your deployment target is a serverless function or a Rack adapter on a CDN edge runtime, boot time and memory matter directly. Sinatra's 50MB baseline beats Rails' 150-200MB comfortably.
Prototyping API contracts: When you need to stand up a fake API server quickly for frontend development or integration testing, Sinatra's lack of ceremony is a feature. You write what you mean without plumbing.
High-traffic stateless endpoints: Sinatra's Rack layer is thin. For services that are pure compute — no database, no external calls — Sinatra can handle more requests per worker than Rails because the middleware stack is shorter. Benchmarks on a 4-core server (ApacheBench, 1000 concurrent connections, simple JSON response) show Sinatra at roughly 8,000-10,000 RPS versus Rails at 4,000-6,000 RPS per process. Neither is a bottleneck at typical loads, but the difference is real.
Where Sinatra will make you regret the choice
Sinatra has no routing namespace by default. For a service with 30 endpoints, your route file becomes hard to navigate without discipline. Rails' controllers and concerns provide a structure that scales with complexity. You can add sinatra-contrib for route namespacing and modular apps, but you are now building framework infrastructure instead of your product.
Database integration is entirely manual. Sinatra does not know what ActiveRecord is. You configure your own ORM connection, your own migration tooling, your own test fixtures. If you are using ActiveRecord anyway (which you can), you have lost most of Sinatra's simplicity while keeping its convention gaps. At that point, Rails is almost certainly the better host for ActiveRecord.
# Sinatra with ActiveRecord gets messy fast
require 'sinatra'
require 'active_record'
ActiveRecord::Base.establish_connection(
adapter: 'postgresql',
database: ENV['DB_NAME'],
host: ENV['DB_HOST'],
username: ENV['DB_USER'],
password: ENV['DB_PASSWORD']
)
# You are now maintaining what Rails gives you for free
Authentication is another gap. Devise does not run on Sinatra. Rolling your own token auth in Sinatra is straightforward; rolling your own session-based auth with security hardening is not.
The practical line
Use Sinatra for services that have three characteristics: fewer than 15 routes, no database persistence layer or a simple one-table read-only query layer, and a clear single responsibility. Anything beyond that and Rails starts paying for its conventions.
The mistake I see most often is teams using Sinatra for something small that grows, and finding themselves six months later with a custom router, a custom ORM integration, a custom authentication middleware — in short, a poorly specified Rails. If you anticipate the surface area growing, start with Rails. If the service genuinely will not grow beyond its initial scope, Sinatra keeps it honest.