Skip to content
Published on

Modern Ruby & Rails in 2026 — Ruby 3.4 / Rails 8 / Hotwire / Sorbet / Kamal 2 / Solid Queue Deep Dive

Authors

1 · Ruby/Rails in 2026 — A Note to Everyone Who Said It Was Dead

Scene from 2026: a friend at YC Demo Day is bragging about his startup. "Built the MVP in three weeks, just hit my first $500K in revenue." Someone asks what stack he used. He half-mumbles the answer. "Rails." A Next.js full-stack engineer at the next table widens his eyes. "Still?"

Still. And in 2026, quieter than ever, running in more places than ever.

The Internet discourse of the early 2020s was "Rails is dead." The reality of 2026 is the opposite — Rails is not dead, and is in fact one of the quietest, hardest-working stacks out there. GitHub, Shopify, Stripe, Airbnb, Basecamp, HEY, GitLab, Square, Coinbase, Cookpad, Mercari, parts of Kakao Pay, parts of Toss. The list is endless.

Why isn't it dead? Three things happened at once.

  1. Ruby 3.4 (December 2024) — YJIT (Yet Another JIT) actually got fast, and error_highlight became the default, making debugging much friendlier.
  2. Rails 8 (November 2024) — 37signals shipped the "Solid trio" (Queue, Cache, Cable) that drops the Redis dependency. You can now run a job queue, a cache, and a WebSocket layer on just Postgres or SQLite.
  3. Kamal 2kamal deploy deploys to any Linux server with Docker in one command. The era of paying hundreds or thousands a month to Heroku/Vercel/AWS Fargate is optional now.

On top of that you have Hotwire (Turbo + Stimulus) — interactive UIs without a React build pipeline. Sorbet (Stripe's gradual type checker) and Tapioca (RBI auto-generator) sprinkle in types. Mission Control monitors jobs without Sidekiq. Propshaft replaces sprockets as the new asset pipeline. Action Notifier is the new Rails 8 notification framework.

This piece walks the whole 2026 Ruby/Rails full stack from top to bottom. Sections 1 to 11 dissect each component. Sections 12 to 14 cover alternatives, the ecosystem in Asia, and "who should pick this." By the end you'll have an answer to whether picking Rails 8 + Kamal over Next.js + Vercel for your next side project is something you'd regret.


2 · Ruby 3.4 (Dec 2024) — YJIT Improvements and Default error_highlight

Ruby 3.4 shipped on December 25, 2024 (Ruby ships a major version every Christmas). Two big headlines.

YJIT — Ruby's JIT Is Real Now

YJIT (Yet Another JIT) is a method-based JIT compiler built by Shopify. It landed experimentally in Ruby 3.1, became production-ready in 3.2, added ARM64 support and trimmed memory in 3.3, and trimmed warmup time again in 3.4. Shopify's own benchmarks show 1.4x ~ 2x average speedup on real Rails workloads. Microbenchmarks like tight arithmetic loops show even bigger numbers, but the meaningful number is on real Rails controller actions.

Enabling it is one line.

# config/boot.rb or an env var
# Top of config/boot.rb
require 'bootsnap/setup' if ENV['DISABLE_BOOTSNAP'].nil?

# Or via env var
RUBY_YJIT_ENABLE=1 bundle exec rails server

Or explicitly in code.

# config/application.rb
require_relative "boot"
require "rails/all"

if defined?(RubyVM::YJIT.enable)
  RubyVM::YJIT.enable
end

# ...

Rails 8 enables YJIT by default in production (unless you explicitly disable it in config/environments/production.rb). To inspect YJIT stats.

# In Rails console
RubyVM::YJIT.runtime_stats
# => {compile_time_ns: ..., compiled_iseq_count: ..., ...}

error_highlight — Friendlier Errors by Default

The error_highlight gem that landed in 3.1 is even more polished in 3.4. When a NoMethodError happens, instead of just "undefined method foo'", it **points to the exact location in your source code with a caret (^`)**.

Old Ruby 1.x ~ 2.x errors looked like this.

NoMethodError: undefined method `name' for nil:NilClass
  app/models/user.rb:42:in `display'

In 3.4 it looks like this.

app/models/user.rb:42:in 'User#display':
    puts "Hello, " + user.profile.name
                              ^^^^^
NoMethodError: undefined method 'name' for nil

You can see at a glance that user.profile is nil. Error tracking services like Sentry / Honeybadger / Bugsnag preserve the caret information end-to-end.

Other Changes

  • The it block parameter is officially supported — [1,2,3].map { it * 2 } is the short form for single-parameter blocks.
  • Range#step now returns an Enumerator instead of a Range.
  • The Prism parser is now the default — a new parser replacing the standard Ruby parser that makes IDE/LSP tooling significantly faster.
  • The GC has more tunable options (MMTk-compatible mode).

3 · Rails 8 (Nov 2024) — Dropping Redis with the Solid Trio

Rails 8 landed in November 2024, and DHH's keynote summed it up.

"We're going Redis-free by default."

For years the standard Rails app infrastructure was Postgres + Redis + Sidekiq. Redis was simultaneously the Sidekiq queue, the Rails cache, and the Action Cable pub/sub. If Redis died, jobs died, the cache died, and WebSockets died. Operating Redis was always a tax on small teams.

Rails 8's answer: Solid Queue, Solid Cache, Solid Cable. All three write to a database (Postgres/MySQL/SQLite). With Redis gone, the dependency tree gets dramatically simpler.

[Traditional Rails 7]              [Rails 8 default]
─────────────────                  ─────────────
Rails app                          Rails app
   |                                  |
   ├── Postgres (data)                └── Postgres
   ├── Redis (Sidekiq queue)               ├── solid_queue_* tables
   ├── Redis (cache)                       ├── solid_cache_entries table
   └── Redis (Action Cable)                └── solid_cable_messages table

A new Rails 8 app can even run all three Solid layers on SQLite by default — close to single-binary simplicity. For a small side project, EC2 t4g.small + SQLite + Solid Queue + Solid Cache + Solid Cable + Kamal is enough.

Creating a new Rails 8 app.

gem install rails -v "~> 8.0"
rails new myapp
# Defaults to SQLite + Solid Queue + Solid Cache + Solid Cable + Propshaft + Importmap + Hotwire

For Postgres.

rails new myapp --database=postgresql

Upgrading an existing Rails 7 app to 8 is its own guide — see Rails Edge Guides' Upgrade Guide.


4 · Solid Queue — What "No Redis" Actually Means

Sidekiq has been the de facto Rails job queue for over a decade. It uses Redis as a backend, pulls jobs with BRPOP, and runs workers via fork+threads. Fast and rock-solid. The downside? You need Redis.

Solid Queue is 37signals' own job queue from HEY and Basecamp, open-sourced. It creates job tables in Postgres/MySQL/SQLite and grabs jobs with FOR UPDATE SKIP LOCKED (or SQLite's transactions).

Installation is straightforward.

bundle add solid_queue
bin/rails solid_queue:install
bin/rails db:migrate

Configure workers in config/queue.yml.

default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 5
      processes: 1
      polling_interval: 0.1

development:
  <<: *default

production:
  <<: *default
  workers:
    - queues: [ critical, default ]
      threads: 10
      processes: 2
      polling_interval: 0.1
    - queues: [ low_priority ]
      threads: 3
      processes: 1
      polling_interval: 1

Job classes use the standard ActiveJob interface.

class WelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

# From anywhere
WelcomeEmailJob.perform_later(user.id)
WelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)

Migrating from Sidekiq to Solid Queue requires almost no code changes (if you were using the ActiveJob interface). But the performance characteristics differ — a Postgres disk queue is roughly 100x slower than Redis's in-memory queue, but plenty fast for small apps. If you're processing hundreds of thousands of jobs per minute, Sidekiq + Redis is still better. But for less than a few thousand per minute, Solid Queue wins on operational simplicity.


5 · Solid Cache + Solid Cable

Solid Cache

37signals' HEY uses terabytes of cache daily. Redis in-memory blew up the RAM bill. The answer? Disk-based cache. With SSDs in the 2020s, disk caches are fast enough.

# config/cache.yml
production:
  database: cache
  store_options:
    max_age: <%= 2.weeks.to_i %>
    max_size: 256.megabytes
    namespace: <%= Rails.env %>

Usage is the same as regular Rails.cache.

Rails.cache.fetch("expensive_query", expires_in: 1.hour) do
  ExpensiveQuery.compute
end

HEY reports average cache hit latency of 1-3ms. Slower than Redis, but with much larger capacity per dollar.

Solid Cable

Solid Cable moves Action Cable's Redis pub/sub dependency to the database. Messages get written to a solid_cable_messages table and delivered to subscribers via LISTEN/NOTIFY (Postgres) or polling (SQLite/MySQL).

# config/cable.yml
production:
  adapter: solid_cable
  connects_to:
    database:
      writing: cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

A controller that sends a chat message.

class ChatMessagesController < ApplicationController
  def create
    message = @room.messages.create!(body: params[:body], user: current_user)

    # Broadcast to other users via Solid Cable
    ChatChannel.broadcast_to(@room, render_to_string(partial: "messages/message", locals: { message: message }))

    head :no_content
  end
end

Good enough for chats with hundreds of concurrent users. For tens of thousands or more, you'll want the Redis adapter (or something like AnyCable).


6 · Hotwire (Turbo + Stimulus) — Interactive Without React

Hotwire stands for "HTML Over the Wire." The philosophy is simple: instead of shipping JSON and rendering it on the client with React, the server ships HTML fragments and the client splices them into the DOM.

It has three parts.

  1. Turbo Drive — intercepts every link click and form submission with fetch, replacing only the <body> of the response HTML. SPA-like page transitions for free.
  2. Turbo Frames<turbo-frame id="cart"> containers that update only a slice of the page.
  3. Turbo Streams — the server sends "append/replace/remove this element" instructions with HTML fragments. Real-time updates over WebSocket or SSE.

Turbo Drive Example

<!-- app/views/posts/index.html.erb -->
<%= link_to "New Post", new_post_path %>

When clicked, Turbo intercepts via fetch and swaps only the response's <body>. SPA feel without writing a line of JavaScript.

Turbo Frames Example

<!-- app/views/posts/show.html.erb -->
<h1><%= @post.title %></h1>

<turbo-frame id="comments">
  <%= render @post.comments %>
  <%= link_to "Add comment", new_post_comment_path(@post) %>
</turbo-frame>
<!-- app/views/comments/new.html.erb -->
<turbo-frame id="comments">
  <%= form_with model: [@post, @comment] do |f| %>
    <%= f.text_area :body %>
    <%= f.submit "Post" %>
  <% end %>
</turbo-frame>

Clicking "Add comment" loads the form only inside the id="comments" frame. The rest of the page doesn't change.

Turbo Streams for Real-Time Updates

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user

  broadcasts_to ->(comment) { [comment.post, "comments"] }
end
<!-- app/views/posts/show.html.erb -->
<%= turbo_stream_from @post, "comments" %>

<div id="comments">
  <%= render @post.comments %>
</div>

Now when any user adds a comment, every user viewing the same page gets the new comment via WebSocket (Solid Cable). No React/Vue required.

Stimulus — Lightweight JS Controllers

For genuinely complex client interactions, you use Stimulus.

<div data-controller="counter">
  <button data-action="click->counter#increment">+</button>
  <span data-counter-target="display">0</span>
</div>
// app/javascript/controllers/counter_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["display"]

  initialize() { this.count = 0 }

  increment() {
    this.count += 1
    this.displayTarget.textContent = this.count
  }
}

HTML is always the source of truth. The opposite philosophy of React's "the virtual DOM is truth, real DOM is derived."


7 · Sorbet (Stripe) + Tapioca — Gradual Types

Ruby is dynamic. Variables don't have types. That's both an asset and a liability. Stripe felt the pain of dynamic typing once their codebase crossed millions of lines, and built Sorbet (open-sourced in 2019).

Sorbet adds gradual types to Ruby. You add a single line to each file.

# typed: true
require "sorbet-runtime"

class User
  extend T::Sig

  sig { params(name: String, age: Integer).returns(User) }
  def self.create(name:, age:)
    new(name: name, age: age)
  end

  sig { params(name: String, age: Integer).void }
  def initialize(name:, age:)
    @name = name
    @age = age
  end

  sig { returns(String) }
  def display
    "#{@name} (#{@age})"
  end
end

# typed: true sets the strictness (ignore / false / true / strict / strong). sig { ... } declares a method signature. At runtime sorbet-runtime validates argument types and raises errors; statically, srb tc does type checking.

bundle add sorbet
bundle add sorbet-runtime
bundle exec srb init
bundle exec srb tc

Tapioca — Sorbet's Automatic RBI Generator

Sorbet reads type signatures from .rbi files (RBI = Ruby Interface). Hand-writing signatures for external gems is hell — Tapioca generates them automatically.

bundle add tapioca --group=development
bundle exec tapioca init
bundle exec tapioca gems    # Generate RBIs for all gems
bundle exec tapioca dsl     # Generate RBIs for Rails DSL (scopes, associations, attributes)

Example RBI generated by Tapioca DSL.

# sorbet/rbi/dsl/user.rbi (auto-generated)
class User
  sig { returns(T.nilable(String)) }
  def email; end

  sig { params(value: T.nilable(String)).returns(T.nilable(String)) }
  def email=(value); end

  # ActiveRecord scope
  sig { returns(ActiveRecord::Relation) }
  def self.active; end
end

Now your IDE catches typos like user.emil. Inside Stripe, over 80% of the codebase is # typed: true or stricter.


8 · Kamal 2 (37signals) — Deploy with kamal deploy

37signals' DHH became famous in 2023 for "We have left the cloud" — a piece about migrating Basecamp/HEY off AWS and saving $7M/year. The tool they built during that move is Kamal (originally MRSK).

Kamal 2 (released 2024) ships your app to any Linux server via Docker + SSH + one command. No Heroku/Vercel/AWS Fargate required.

gem install kamal
kamal init

Define your deployment in config/deploy.yml.

service: myapp

image: myteam/myapp

servers:
  web:
    hosts:
      - 1.2.3.4
      - 1.2.3.5
  job:
    hosts:
      - 1.2.3.6
    cmd: bin/jobs

registry:
  server: ghcr.io
  username: myteam
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  clear:
    RAILS_ENV: production
    RAILS_LOG_TO_STDOUT: 1
  secret:
    - RAILS_MASTER_KEY
    - DATABASE_URL

accessories:
  postgres:
    image: postgres:16
    hosts:
      - 1.2.3.4
    env:
      secret:
        - POSTGRES_PASSWORD
    volumes:
      - /var/lib/postgresql/data:/var/lib/postgresql/data

proxy:
  ssl: true
  host: myapp.com

Deployment is one line.

kamal deploy

What does that do.

  1. Build the Docker image locally (or pull one built in GitHub Actions).
  2. Push to the registry.
  3. SSH into every server and pull the new image.
  4. Spin up new containers; Kamal Proxy (the successor to Traefik) handles the traffic swap for zero-downtime deployment.
  5. Clean up old containers.

The first time you deploy, you run kamal setup instead — it installs Docker on the target machines and does the initial deploy.

kamal setup       # Install Docker + initial deploy
kamal deploy      # Every subsequent deploy
kamal rollback    # Roll back
kamal logs        # Tail logs
kamal app exec --interactive --reuse "bin/rails console"  # Open a console

On a single EC2 t4g.medium (30/month)youcanrunPostgres+SolidQueue+yourRailsapproughlyHerokuhobbytierprice(30/month) you can run Postgres + Solid Queue + your Rails app — **roughly Heroku hobby tier price (25/month) on a far more powerful instance**. When traffic grows, add servers and kamal deploy --hosts ... to horizontally scale.


9 · Mission Control — Rails-Native Job Monitoring

Sidekiq always had a great web UI (/sidekiq). Move to Solid Queue and you lose it? No — Rails 8 ships an official web UI called Mission Control - Jobs.

bundle add mission_control-jobs
# config/routes.rb
Rails.application.routes.draw do
  mount MissionControl::Jobs::Engine, at: "/jobs"
  # ...
end
# config/application.rb
config.mission_control.jobs.base_controller_class = "AdminController"  # auth

Visit /jobs to see queue state, in-progress jobs, failed jobs, retries, and pause/resume controls. Almost all of Sidekiq's web UI features, working on every Solid Queue adapter (Postgres/MySQL/SQLite).

Companion tools for job observability.

  • Skylight / Scout APM — memory/CPU profiling per job.
  • Sentry / Honeybadger — error tracking and alerts on job failures.
  • Datadog — direct collection of Solid Queue table metrics.

10 · Propshaft / Action Notifier — New Rails 8 Defaults

Propshaft — The Sprockets Successor

Sprockets has been the Rails asset pipeline since Rails 2 — compiling CoffeeScript/Sass/ERB into JS/CSS and adding digest fingerprints for caching. But in the 2020s, esbuild, Vite, Bun, and importmap-rails do transpilation better. Propshaft doesn't transpile at all — it just fingerprints and serves files.

# Gemfile
gem "propshaft"
# Generate fingerprinted files
bin/rails assets:precompile

The new asset pipeline philosophy.

  • Transpilation is the job of esbuild/Vite/jsbundling-rails.
  • Bundling is also their job.
  • Propshaft only fingerprints and serves.

A new Rails 8 app defaults to Propshaft + Importmap-rails + Hotwire. JavaScript is loaded via importmap natively in the browser; CSS gets fingerprinted by propshaft.

Action Notifier (New in Rails 8)

Rails 8 introduces a first-class notification system. A standard abstraction for sending the same notification across email, SMS, push, Slack, database, and other channels.

class NewCommentNotifier < ApplicationNotifier
  deliver_by :email, mailer: "CommentMailer", method: "new_comment"
  deliver_by :slack, channel: "#engineering"
  deliver_by :database

  param :comment, :user

  def message
    "#{params[:user].name} added a new comment."
  end
end

# Call it
NewCommentNotifier.with(comment: @comment, user: current_user).deliver(@post.author)

Puts the common pattern of fanning out one notification to multiple channels in one place. Inspired by the Noticed gem and formalized in Rails 8.


11 · RuboCop / Standard / Brakeman — Static Analysis

RuboCop — The Most-Used Linter

RuboCop is Ruby's ESLint equivalent. It catches everything from style violations to potential bugs.

bundle add rubocop --group=development
bundle exec rubocop
bundle exec rubocop -a   # Autofix

Extension gems like rubocop-rails, rubocop-rspec, rubocop-performance broaden coverage.

# .rubocop.yml
require:
  - rubocop-rails
  - rubocop-rspec
  - rubocop-performance

AllCops:
  TargetRubyVersion: 3.4
  TargetRailsVersion: 8.0
  NewCops: enable

Style/StringLiterals:
  EnforcedStyle: double_quotes

Layout/LineLength:
  Max: 120

Standard — Opinion-Free RuboCop Wrapper

If configuring .rubocop.yml every time is exhausting, use Standard. It wraps RuboCop with a "we've already decided everything, just behave" philosophy.

bundle add standard --group=development
bundle exec standardrb
bundle exec standardrb --fix

Used by Tenderlove (Aaron Patterson) and many OSS maintainers. Standard does for Ruby what Prettier did for JavaScript.

Brakeman — Static Security Analysis

Brakeman is a Rails-specific static security analyzer. It catches SQL injection, mass assignment, XSS, command injection, and other common vulnerabilities.

bundle add brakeman --group=development
bundle exec brakeman

Wire it into CI to run on every PR. A GitHub Actions example.

name: Brakeman
on: [pull_request]
jobs:
  brakeman:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.4
          bundler-cache: true
      - run: bundle exec brakeman --no-pager

A new Rails 8 app generates RuboCop, Brakeman, and a GitHub Actions workflow by default.


12 · Alternatives — Hanami 2 / Roda / Sinatra / dry-rb

Rails isn't the only answer. The Ruby ecosystem has other good choices.

Hanami 2

Hanami aims to be "a more explicit, more modular framework than Rails." Hanami 2 (released 2022) made it production-viable. Highlights.

  • Dependency injection (DI) container as a first-class citizen, built on top of dry-system.
  • Router / controller (action) / view / view model explicitly separated.
  • "Explicit over conventional" philosophy. Less magic.
# slices/main/actions/books/index.rb
module Main
  module Actions
    module Books
      class Index < Main::Action
        include Deps[
          "repositories.book_repo"
        ]

        def handle(_request, response)
          response[:books] = book_repo.all
        end
      end
    end
  end
end

A good fit for small-to-medium APIs or SaaS. Steeper learning curve than Rails, but shines when an explicit structure pays off in a large codebase.

Roda

A micro routing-tree framework by Jeremy Evans (author of Sequel). Extremely fast, with tree routing that lets routes nest naturally.

require "roda"

class App < Roda
  plugin :json
  plugin :halt

  route do |r|
    r.root { { hello: "world" } }

    r.on "users" do
      r.get Integer do |id|
        { user_id: id }
      end

      r.post do
        # new user
      end
    end
  end
end

Faster than Sinatra, lighter than Hanami. Pairs well with libraries like Shrine and Sequel.

Sinatra

The oldest micro-framework. Still great for simple APIs and small tools.

require "sinatra"

get "/hello/:name" do
  "Hello, #{params[:name]}"
end

dry-rb / Rom-rb

dry-rb is the library collection Hanami is built on. dry-validation (schema validation), dry-monads (Result/Maybe monads), dry-types (types), dry-system (DI container), dry-effects (effects). Usable with Rails too.

Rom-rb is a data-mapper-pattern ORM. Unlike ActiveRecord, it separates domain objects from persistence. Attractive for teams doing DDD seriously.


13 · Korean / Japanese Ruby Ecosystem — Cookpad, Mercari, Kakao Pay

Japan — Ruby's Birthplace

Ruby was created by Matz (Yukihiro Matsumoto) in 1995. Japan is Ruby's birthplace, and Japanese companies have the deepest, oldest Ruby/Rails usage.

  • Cookpad — Japan's largest recipe site. Almost the entire backend is Rails. Main sponsor of RubyKaigi. Frequently shares how they run their internal monolith at Ruby/Rails conferences.
  • Mercari — Japan's largest secondhand marketplace. A large portion of the backend is Ruby/Rails; some parts are migrating to Go/Python. Even the Go migration moves business logic that started in Ruby slowly over time.
  • GMO Pepabo — live commerce / e-commerce.
  • Money Forward — household accounting / finance SaaS. Started on Rails and grew up.
  • Sansan — business card management. Rails-based.
  • freee — accounting SaaS. Took Rails to IPO.

Japan holds RubyKaigi every year (a 2-3 day conference) with Matz giving the keynote himself. Talks tend to be about Ruby itself rather than Rails — deep discussions about MRI / YJIT / language design are the norm.

Korea — Rails Usage Is Growing

Korea's Rails usage isn't as deep as Japan's, but it's growing steadily.

  • Kakao Pay — parts of the backend on Rails. Some domains around payments / membership.
  • Toss — internal tools and some internal systems on Rails.
  • Bridge Plus / D.CAMP — accelerators often see startups starting with Rails.
  • Known Korean Rails examples — Class101, Mirinae, parts of Zigbang's back office, and others.

Korea had an active RubyKR conference in the mid-2010s. The current RubyKR Slack/Discord community is small but active. Rails developer hiring in Korea isn't as common as in Japan but is steady, especially for senior or startup tech-lead roles.

Global — Well-Known Examples

  • Shopify — the world's largest Rails app. Main sponsor of YJIT. Many Rails core contributions.
  • GitHub — started on Rails, still a large portion on Rails. The reference example for running a monolith.
  • Stripe — birthplace of Sorbet. A large portion of payment processing backend is Ruby.
  • Airbnb — Rails from the start. Some parts migrated to Java/Kotlin, some still Ruby.
  • Basecamp / HEY — 37signals' own products. Where DHH's vision is directly implemented.
  • GitLab — Rails. Both self-hosted and SaaS.
  • Twitch — started on Rails, some parts migrated to Go/Erlang.
  • Coinbase — started on Rails, some parts migrated to Go.

14 · Who Should Pick Ruby/Rails — Solo / Startup / B2B SaaS

Solo / Side Project

Overwhelmingly, I recommend Rails. Why.

  • Speedrails new gives you auth, job queue, cache, websocket, migrations, tests, and admin scaffolding in one command.
  • Infra simplicity — Rails 8 + the Solid trio + SQLite fits on a single EC2 t4g.small.
  • Kamal makes deploy cost nearly zero — $5/month to start without Heroku/Vercel.
  • No frontend build pipeline thanks to Hotwire — zero Node dependencies.

Faster and cheaper than wiring up Next.js + Vercel + Postgres + Upstash Redis + Resend + Stripe — just Rails + Kamal + Postgres.

Early Startup (1-15 people)

Rails is still the best pick. 37signals, GitHub, Stripe, Shopify all started on Rails and grew to IPO/unicorn. The cohesion of a full-stack framework is absolutely critical during the PMF stage. Next.js's monorepo + RPC + auth library + job queue dependency tree is a burden when you're still looking for product-market fit.

A good combo.

  • Rails 8 + Postgres + Solid trio + Hotwire + Kamal 2.
  • Sentry for error tracking, Skylight or Scout APM for APM.
  • RSpec or Minitest, FactoryBot, Capybara/Cuprite for tests.
  • Gradual Sorbet adoption (don't go strict everywhere; start with the core domain).

B2B SaaS (15-100 people)

Still Rails. But pay more attention to types and domain separation.

  • Use Sorbet to type the core domain.
  • Use Service Object / Command patterns to avoid fat controllers.
  • Adopt parts of Trailblazer, dry-rb, or Hanami patterns.
  • Keep the monolith but separate domains via Engines (Shopify's "Modular Monolith" pattern).

100+ People / Microservice Stage

Rails still works fine, but at this scale organizational design matters more than language choice. If your Rails monolith needs to be naturally decomposed, it's worth considering moving some services to Go/Java/Kotlin (like Shopify keeping core on Rails while migrating some infrastructure services to Go/Rust).

When to Avoid Rails

  • Ultra-low latency (<10ms) is your business, like game servers, high-frequency trading, embedded. Go, Rust, Elixir, C++ are better.
  • Machine learning inference/training pipelines are your main job — Python is the answer.
  • Types are absolutely mandatory in your domain — Sorbet can get you there, but TypeScript/Kotlin/Rust is a more direct path from day one.
  • Already a single-language Node/Python shop — no reason to add Ruby.

15 · References