As the holidays near, it’s time to give yourself (and your team) the gift of a happier development experience! So, over 12 days, we’ll unwrap 12 small (but mighty) approaches designed to level up your Rails apps. We’ll discuss, then suggest hands-on practice. These bite-sized tasks are designed to be completed in under an hour. They’ll need to be quick if we’re going to convince Santa to make the switch from “Sleighs” to Rails—he’s a busy dude!
Irina Nazarova CEO at Evil Martians
- Day 1: a renewing, anti-aging treatment
- Day 2: faster rails boot time
- Day 3: better yourself and better your environment
- Day 4: flaky flakes
- Day 5: the nutcracker suite of system testing
- Day 6: a test suite so fast, Santa’s elves are jealous
- Day 7: better database migrations
- Day 8: silent night, secure night
- Day 9: Dashing through the …queries
- Day 10: deck the docs with lines of clarity
- Day 11: Jemlocking around the Christmas tree
- Day 12: Silent night, stable night
Day 1: a renewing, anti-aging treatment
A libyear is a simple metric that shows your application’s “freshness”. In other words, it shows you how outdated your dependencies are–literally–in years.
Let’s generate a libyear report for one of your current applications:
# Install libyear
$ gem install libyear-bundler
# Generate libyear report
$ libyear-bundler
aasm 5.2.0 2021-05-02 5.5.0 2023-02-05 1.8
actioncable 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionmailbox 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionmailer 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionpack 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actiontext 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionview 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activejob 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activemodel 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activerecord 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activestorage 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activesupport 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
acts_as_list 1.1.0 2023-01-31 1.2.4 2024-11-19 1.8
...
System is 663.4 libyears behind
OK, so we’re 663.4 libyears behind–that’s not so bad! Actually, for larger apps, hitting the century mark is pretty easy. But don’t panic: nothing here is outdated… it’s just “vintage”!
(Yes, “vintage” like that disgusting bottle of long expired eggnog. But hey, eggnog is always festive, and this post has a holiday theme, so keep that in mind! 🎄)
In any case, at first glance, “libyears” might seem like a novelty metric rather than a useful one, but it’s actually great for comparing different subprojects and prioritizing upgrades.
Moreover, regularly running this gem encourages developers to update those small, forgotten gems (or find modern alternatives), instead of just focusing on the big and interesting ones like Rails.
Additionally, another benefit of the libyear rejuvenation treatment is that new versions often bring bug fixes and speed boosts for free—just a little love and attention needed!
Don’t let libyears catch up with you
Today’s holiday challenge is to dust off an old app where the libyears are starting to look more like lightyears.
(NOTE: above, the intended association of “lightyears” is, of course, being expressed with the vibes of “very old”, using “lightyears” as a measurement of time, rather than distance, which some readers would have reasonably anticipated.)
(SECOND, MORE RELEVANT NOTE: one common oversight is gems with stalled custom GitHub links or pinned versions due to bugs. Developers often forget to revert back to the upstream after the fix has been applied. Missing out on those precious updates from developers has never been easier!)
So, let’s update some of those gems! But wait—don’t just rashly run bundle update—that’s too much extra stress for holidays! Instead, let’s keep things merry and bright by updating just a couple of gems at a time.
But how do you decide what to update? While tools like libyear can provide guidance, let’s unwrap another gift from Santa’s sack to add to your holiday toolbox: bundle_update_interactive.
Not only does this gem make the updating process more interactive and sophisticated, it also lets you quickly review the changelog–with just one click! No more hangover regrets after those fancy, reckless upgrade parties (again, presumably facilitated via eggnog, the most festive of drinks).
Day 1 festive action plan:
- Generate a libyear metric and save the results before then making improvements.
- Check all gems with pinned versions or GitHub sources.
- Run bundle_update_interactive and update some of your gems.
- Once you’re happy with the results, generate a libyear metric again, calculate the difference, and share your achievements under #railsmas!
It would be great to see you challenge the Rails community with your results—let’s see who’s got the youngest, the oldest, or the most rejuvenated app!
Day 2: faster rails boot time
One of the worst parts of the holiday season most people are afraid to admit: waiting for the presents. (Best part? Presents.) “What am I gonna get?” This is practically all one can think as you wait and wait throughout the year.
…but you know the kind of waiting that is even worse? Waiting for your Rails application to load.
So, today, we’ll focus on speeding up your boot time! It’s the gift that will keep on giving, whether for a code reload in development or a deployment in production.
Santa also does the whole “boot” thing
Before diving into specifics, let’s try to understand your personal boot time a little better. Take a look at your application now. To do so, use this shiny new CRuby profiler: Vernier.
First, let’s get Vernier installed and profile our boot process:
# Install Vernier sampling profiler
$ gem install vernier
# Capture a profile of the application's full boot process.
$ DISABLE_SPRING=1 vernier run -- bundle exec rails runner 'puts "Railsmas is here!"'
Railsmas is here!
#<Vernier::Result 2.821947994 seconds, 7 threads, 3943 samples, 3103 unique>
written to /tmp/profile20241126-134618-4btew9.vernier.json.gz
Every application is unique as a (oh, let’s say) snowflake, and depending on the duration of the boot process, you’ll either get a very clear or really messy graph.
Don’t forget to also generate a second report with eager loading enabled inside config/environments/development.rb
, as it provides more data that can draw out valuable insights:
config.eager_load = true
Now open the generated profile in the Vernier UI:
When working with the call tree, always take note of the percentage of the report spent during the boot for each subtree.
Note: it’s more effective to dig into the largest parts first. Pay attention to the big initializer spans where the highest percentage of boot runtime is spent during loading.
We should always remember that during loading, we’re running all the initialization and class-level code. Some examples: reading huge JSON files, eager-loading all time zone information, attempting to use i18n on a class level which eager loads all translations, and so on.
Extensive DSLs are also often common offenders: Active Admin, Grape, GraphQL, RSwag, as well as many others. We may try to separate loading these large parts of the app into separate instances on production or skip them locally when we’re not working on these parts.
External dependencies might contain all those issues as well, so the key here is to audit your Gemfile. Do you really need all those gems? Unused gems add unnecessary boot time and increase complexity. Slim things down, and give your application the gift of simplicity!
You could also move some gems into separate groups to load them conditionally.
Santa’s gift
Now for the main action. Santa, a magical being who has powers both enigmatic and indecipherable, has come with a token of festive altruism: a one-line change in Devise’s configuration that can significantly improve your boot time. (By default, Devise forces all application routes to be reloaded, effectively loading them twice.) However, we can easily disable this default behavior in config/initializers/devise.rb
:
# When false, Devise will not attempt to reload routes on eager load.
# This can reduce the time taken to boot the app but if your application
# requires the Devise mappings to be loaded during boot time the application
# won't boot properly.
config.reload_routes = false
Of course, the impact of this change scales with the number of routes in the application, but it should provide a noticeable improvement in load time.
Day 2 festive action plan:
Install Vernier or rbspy (Ruby < 3.2.1) and create two profiles for your application: one with eager loading enabled; one without eager loading.
Analyze the generated profiles in Vernier UI:
- Explore the call tree for the most time-consuming subtrees and methods.
- Pay close attention to the percentage of the report spent during the boot for each subtree.
Find the offenders and optimize them:
- Consider moving large parts of the app into separate instances in production or skipping them locally.
- Remove unused gems from your Gemfile.
- Move some gems into separate groups to load them conditionally.
- Disable Devise’s route reloading if applicable.
Ensure the application loading remains functional by testing your application after making the changes and share your 2nd day insights under #railsmas!
Ho ho! What did you think of Santa’s boot speed prowess?
Day 3: better yourself and better your environment
New Year, new vibes! Yesterday, we gave our app a bit of an energy boost, so today, let’s focus on improving our development setup! Suddenly, DHHanish Santa suggests we should migrate to a brand new Linux-endorsed notebook—tempting, but let’s not go there just yet.
Today, we’re thrilled to unwrap a special gift that was actually prepared by Shopify two years ago. Rest assured, this refurbished gem is truly a treasure—Ruby LSP!
What makes this so special? After all, we’ve had tools like RubyMine and Solargraph for years—who needs another Language Server Protocol (LSP) implementation for Ruby?
The answer lies in one immensely powerful feature: an addon system written in Ruby. While ruby-lsp
includes advanced staples like jump-to-definition, inline documentation, and code navigation, the addon system takes it even further by allowing anyone to easily expand its functionality.
The real power, however, lies in distributing these addons—you don’t even need to install addons manually—they load automatically (like Railties) when placed inside ./lib/ruby_lsp/**/addon.rb
of your gem. This means every user of both Ruby LSP and your gem enjoys an enhanced editor experience without any extra effort!
Just imagine it: all the convention-over-configuration magic is unveiled—the dark sorcery behind ActiveModelSerializer, CanCanCan, ActiveAdmin, become just a bunch of people in strange costumes.
And keep in mind the LSP API reminds RuboCop cops—and, of course, thanks to Ruby—creating addons becomes straightforward.
You could even develop a custom application LSP addon to highlight your application’s unique DSLs before midnight on New Year’s Eve!
Here’s a little miracle—a sneak peek of a plugin for our beloved Action Policy gem to enable simple jump-to-definition support for its DSL. Now we can effortlessly jump from a controller to the relevant policy and back:
Wow, it works! Even better, this plugin would work for every Ruby LSP user if we contribute our new code to the upstream gem.
Day 3 festive action plan:
- Set up Ruby LSP in the editor of your choice.
- Explore the new, advanced features available right out of the box, including all the standard yet more powerful staples—for example, effortless navigation to hidden elements within Rails’ “convention over configuration” motto—and much more.
- Consider creating a simple plugin for your application’s (or a beloved gem’s) custom DSL to help your colleagues save time working with the code.
By the way, share your Ruby LSP ideas under #railsmas—your suggestions might inspire others! After all, the holidays are all about coming together. (And, of course, the presents.)
Day 4: flaky flakes
Every year, we hear (and absolutely adore) the same tired holiday jingles. We simply can’t get enough of them!
Some are quite catchy, and we find ourselves mentally looping through them. We need something to fight this kind of repetitiveness, like our gift for today: dealing with flaky tests, which force you to repeat test suite runs again and again. And we all loathe this repetitiveness, just as much as any nefarious holiday earworm.
First of all, we should start with proper test configuration to catch the flakies early. Check that you’re running the tests in the random order:
RSpec.configure do |config|
config.order = :random
Kernel.srand config.seed
end
This prevents you from relying on subtle global state cleanup sequences (which are sometimes found in applications’ test suites). And if you have a moderate test suite without this option, anticipate lots of unexpected failures. Embrace the holiday chaos, unpleasant at first, presents later.
By the way, what’s all the fuss about global state leaks? Basically, anything changed during a test, except for database interactions (safeguarded by default with transactional tests or old-school database_cleaner), is at risk of retaining lingering state and leaking into other test contexts.
The lengthy list of common culprits include:
- ActiveRecord models created outside transactions (such as inside
before(:all)
) - ENV changes
- Configuration settings (app’s config, routes, logger, I18n, etc.)
- Global, class or singleton level variables
- Filesystem modifications
- Additional data stores such as Redis, Elasticsearch, RabbitMQ, etc. (if you use real integrations in tests)
- Cache / in-memory stores
- Different queues such as for jobs, mailers, etc. (always prefer fake modes for performance benefits)
- Browser storages for system tests
- Database primary key counters
OK, got global state under control, but still experiencing flakiness? Like a brutal holiday flight delay-someone has messed up their Time
!
If inside the test we interact with time, it should be frozen at a specific time point (and always if it’s time-related asserts here).
Another common oversight is tests that are dependent on external systems or services, as it’s unstable and slow. Places to assess for risks in apps:
- HTTP requests to external APIs (including gems of services with hidden HTTP facade)
- Remote file storage access
- SMTP mailing
- DNS resolutions
- Raw sockets usage
Try to disable external requests altogether by bringing in WebMock into the test suite. Remember: Good tests are like Santa’s workshop-self-contained and not dependent on the outside world! (Disclaimer: unconfirmed lore.)
Last but not least, ordering! Here’s a self-explanatory snippet:
RSpec.describe 'Delivery order' do
it 'is a naughty test!' do
expect(Gift.all).to eq([gift1, gift2]) # Ho-ho-NO! Order undefined!
end
it 'is a nice one!' do
expect(Gift.order(:id)).to eq([gift1, gift2]) # Santa approved!
end
it 'is an extra nice test!' do
expect(Gift.all).to match_array([gift1, gift2]) # Santa’s secret method
end
end
A random endnote appears! Randomness isn’t always good, it also might cause havoc. Sometimes you need to use stable values, for example when using VCR cassettes.
Day 4 festive action plan:
- Ensure you’re running tests in random order.
- If you know you have a flaky test—choose one and try to fix it using our list of common problems.
- Don’t have one? We don’t believe your sweet lies!
Search forDate
andTime
usages in your tests and check if they use frozen time. - Try to cut your test suite from the Internet by installing the WebMock gem.
- Fortify your test suite by utilizing
match_array
or ordering in proper places.
Day 5: the nutcracker suite of system testing
Besides piles of presents, the holidays also present an opportune occasion for massive test parties, holiday gatherings where the main event is invariably… failure (because, well, system tests are involved). But don’t worry, help is just under the tree!
The plan to save the party consists of several parts.
Part one: being prepared to accept the inevitable-system tests assert more expectations on average, cover lots of application’s parts, run inside a live browser environment, and thus naturally prone to test breaches. Therefore, the crucial step is minimizing time spending developers waiting for the whole suite reruns.
The simplest trick here is to retry individual tests instead of the whole test run. We have rspec-retry and Minitest::Retry for corresponding test frameworks. This represents a sharp knife to approach the problem with, so our recommendation is to be cautious and include it only for system tests to avoid hiding genuine issues in unit tests. (Also, do not forget to limit the amount of retries, so we do not balloon the whole test suite run with so-so tests.)
Simple trick: the day after
Since we’ve already saved a lot of time by skipping manual test suite retries, we can now move on to part two: actually fixing the flakiness of the affected system tests. And just like disrespectful children trying to steal a peak at the elusive Santa on Christmas night, we can try to catch our failures using Rails automatic screenshotting. (Pro tip: do not forget to persist the failure screenshots on CI too.)
One Viewport size fits all
Speaking about screenshots, it’s important to set up a proper viewport size. This configuration depends on a particular driver-for an example with the now-hot Cuprite, check out our blog post. As a bonus, it also provides more stable tests as everyone will receive the same behaviour in scrolling pages.
Capybara matchers: the good, the bad, and the flaky
Having a rock solid configuration is not enough-you’ll need to be careful with test code. Capybara matchers are smart, but we have to be careful not to miss all conditions in the selector:
# Naughty developers use RSpec matchers that flake like artificial snow ❄️
# This matcher may find a Santa list before it’s filled by the elves.
text_field = find('.santa_list)
expect(text_field['value']).to match /Nice list/
# Nice developers make Santa happy with synchronized Capybara matchers 🎅
# This matcher retries until the elves finish updating the list!
expect(page).to have_field('.santa_list, with: /Nice list/)
Sleepless nights with Capybara Lockstep
While Capybara can retry some failed actions (like element expectations), it doesn’t know anything about JavaScript nor AJAX, so UI interactions, like modal forms or dynamic updates, where actions may be attempted before elements are ready (and network requests are still in flight) are prone to failure, depending on timing and machine performance.
Usually, developers try to solve it with explicit sporadic sleep
s. But there’s a better alternative: capybara-lockstep.
With the gem configured, Capybara’s new behavior means it waits for all JS async interactions to be completed before executing the next matcher. So, we have a guarantee that the page is ready which means we don’t need any unreliable sleep
s between test steps. capybara-lockstep also enhances test performance by intelligently skipping unnecessary waits, eliminating the need for inefficient fixed delays. It’s such a pivotal experience that we believe it should be included in every system test suite by default!
Day 5 festive action plan
- Install rspec-retry or Minitest::Retry to retry individual tests instead of whole suite runs.
- Ensure system tests failure screenshots are available.
- Set up a proper browser viewport configuration for more stable tests.
- Verify system test matchers to make sure they don’t contain incomplete conditions.
- Install capybara-lockstep to eliminate necessity in explicit unreliable slow
sleep
calls.
Share other tips and cautionary tales about failing system tests with #railsmas!
Day 6: a test suite so fast, Santa’s elves are jealous
As the holiday season approaches, time seems to turn slower for all the excited, entitled young youngsters as they eagerly await the thrill of ripping open the paper separating them from their sweet, sweet presents. This wait can be skincrawlingly brutal. Perhaps this phenomenon is somewhat similar to what a developer experiences while waiting for the last bits of a test suite to finish before they can smash the merge button.
So, to avoid a visit from the Spirit of Christmas Impatience (a character that Dickens left on the cutting room floor), today we’re unwrapping strategies to speed up your tests. Because why not make the development process as brisk and pleasant as a winter breeze?
First, let’s identify the bottlenecks. Pinpointing slow tests is the first step to a brighter, faster suite. To start, enable profiling and let’s shed some light on those sluggish specs:
# For RSpec:
bin/rails spec --profile
# Or for Minitest enjoyers (Rails 7.1+):
bin/rails test --profile
This will show you the slowest tests from your suite, but, just from this alone, we don’t really know why they’re slow (unless you put sleep christmas.from_now
there). To dive deeper into how your tests work, use TestProf. It’s like a toolkit of miracles for your test suite, revealing not only issues of a particular test, but also systemic inefficiencies of the whole test suite that can save you significant time.
Once you’ve identified the culprits slowing down your test suite, it’s time to optimize. Let’s talk about some key areas where some fine-tuning and small changes can make a big difference in your testing cadence.
First, ah, Devise—the trusty guardian of our user data. Its encryption in tests can be a bit like that overzealous relative who insists on wrapping every gift with triple layers of tape. Consider using a speeding up hashing algorithm in your test environment:
# config/initializers/devise.rb
Devise.setup do |config|
# Use a lower cost factor in tests
config.stretches = Rails.env.test? ? 1 : 12
end
Devise’s algorithm takes a lot of time—are you surprised? Logs are yet another “Grinch”, heartlessly stealing our precious test suite time. While they can be useful during tests, they can also be excessive. To speed things up, consider reducing your logs’ verbosity, or disabling them entirely.
# config/environments/test.rb
unless ENV['WITH_LOGS']
config.logger = Logger.new(nil)
config.log_level = :fatal
end
Don’t underestimate this small change because it might cut up to 10% from the total test suite running time.
Coverage tools fall into the same category. Run them selectively, perhaps only in CI, to keep your local tests running fast:
if ENV['COVERAGE']
require 'simplecov'
end
Another culprit is the logic buried inside callbacks. One common oversight is PaperTrail, which is great for tracking changes, but during tests, it can feel tracking every snowflake in a blizzard instead of just some more conventional way of doing the weather forecast. Turn PaperTrail off to streamline your test runs.
Background jobs also can be sneaky time sinks. Make sure you’re using Sidekiq::Testing.fake!
mode to skip job processing without the overhead of actual execution, keeping your tests focused and fast.
External HTTP requests also slow everything down! Use VCR to record and replay HTTP interactions, ensuring your tests run smoothly without waiting for the network. VCR cassettes also fortify tests against intermittent test failures.
Day 6 festive action plan
- Turn on test profile and try to optimize the slowest one
- Install TestProf and take a look at the profile of your suite, try to fix global issues first.
And as a reminder, here are a list of common issues:
- Devise hashing cost factor
- Logs and Coverage
- Logic in callbacks (i.e. PaperTrail)
- Disabled Sidekiq fake mode
- External requests
Then, once you’ve done that share your own tips and findings on speeding up tests with #railsmass. We want to see your speedshop results!
Day 7: better database migrations
As we continue Railsmas, it’s time to give our database toolset a makeover. Not having your database in order can cause cascading problems, like watching a house of cards tumble helplessly apart. Or, to keep with the theme, more like watching a gingerbread house pathetically crumble because you didn’t do something right.
Anyway, let’s start with migrations, since they can introduce risks if not handled carefully. For instance, running some migrations on large tables can lock your database, causing downtime.
On this count, the strong_migrations
gem is the best gift you can give your team. This gem acts as a safety net, catching potentially dangerous migrations before they wreak havoc on production:
bundle add strong_migrations
bin/rails g strong_migrations:install
Next, let’s tackle database consistency. The database_consistency
gem is like your holiday checklist, and you can be sure that your database constraints and model validations are aligned, which prevents discrepancies that could lead to bugs, making your application, well, better.
bundle add database_consistency --group development --require false
bundle exec database_consistency
NullConstraintChecker fail User code column is required in the database but does not have a validator disallowing nil values
NullConstraintChecker fail User company_id column is required in the database but do not have presence validator for association (company)
...
Over time, your migration files can accumulate like decorations in the attic, both cumbersome to manage. This is where the squasher gem comes in handy. It allows you to consolidate old migrations into a single file, simplifying your migration history.
gem install squasher
# Squash all migrations prior to 1st January 2024
squasher 2024 -m 8.0 # for Rails 8.0
Finally, let’s introduce a “little elf” that will keep your database as organized as Santa’s workshop! The actual_db_schema
gem automates migrations on branch switches. This gem ensures your database schema remains consistent across branches by automatically rolling back WIP migrations; it saves you from the hassle of manually managing schema discrepancies when switching branches.
Day 7 festive action plan
- First and foremost introduce
strong_migrations
to your workflow. - Sync database checks and model validations with
database_consistency
. - Consider squashing old ActiveRecord migrations with squasher.
- Simplify working with multiple branches locally with
actual_db_schema
.
Then, share your best practices in database maintenance using #railsmas!
Day 8: silent night, secure night
On the eighth day of Railsmas, we’re wrapping our apps in a warm, cozy blanket of security—because nothing ruins a celebratory spirit faster than a data breach. So, let’s “deck the halls” with some security best practices and tools.
Audit all the way
bundler-audit
and ruby_audit
scan your dependencies for known vulnerabilities, ensuring your app doesn’t rely on insecure versions of libraries. Make sure you’re running them regularly on CI:
gem install bundler-audit ruby_audit
bundle-audit check --update
ruby-audit check
(Feeling extra festive? Let Dependabot check and handle dependency updates for you.)
Brakeman: let it scan, let it scan, let it scan
Brakeman is the ultimate security scanner for Rails apps, and it’s been a trusted tool for years. It scans your codebase for vulnerabilities like SQL injection, cross-site scripting (XSS), and other common issues, helping you identify and fix problems before they become serious.
gem install brakeman
brakeman
(By the way: Starting with Rails 8, the rails new
command automatically adds Brakeman to your CI pipeline. So if you’re spinning up a new app, Brakeman will already be keeping your app secure.)
Outdated configurations: the ghosts of Rails past
Over time, best practices evolve, and it’s easy for old settings to linger in your app, quietly undermining your security.
The Rails Security Guide is your go-to resource for identifying outdated or insecure configurations. Take some time to review your app’s settings and make sure they align with current best practices.
And don’t forget about your dependencies! For example, if you’re using Devise for authentication, check your config.stretches
setting against the latest default value, at the time of writing, the recommended number of stretches is 12. Anything lower, and it’s time for an update:
# config/initializers/devise.rb
config.stretches = Rails.env.test? ? 1 : 12
Note, it will only affect new passwords generated after the change. Existing users will continue using their original stretch count until they change their passwords.
Day 8 festive action plan
- Add
bundler-audit
andruby_audit
to your CI (or use Dependabot). - Run Brakeman to scan your app for vulnerabilities.
Then, review your app for outdated configurations:
- Review your app against the Rails Security Guide.
- Check configurations of security related dependencies like Devise, Lockbox, and so on.
And don’t forget to share your findings with the hashtag #railsmas—but only after you’ve patched those zero days, because Santa’s watching!
Day 9: Dashing through the …queries
On the ninth day of Railsmas, it’s time to make our databases faster than Santa (who has to be very, very fast, as he must go further than the distance from the Earth to the Sun within one night). This time: tools, tips, and tricks to optimize your database performance so your app can handle the holiday rush (or any rush) without breaking a sweat.
PgHero: your friendly neighborhood database tool
When it comes to database optimization, your configuration is the best place to start. PostgreSQL is notorious for being configured out-of-the-box to run with conservative defaults–perhaps a great to sort through good/naughty lists on Santa’s ancient computer, but it’s not exactly up to snuff for modern speed demands. We think PgHero is a must-have tool. It provides a dashboard with insights into your database’s health, including config recommendations, unused indexes, query stats, and more.
For extra visibility into your slow queries, consider automatically adding comments to them to make it easier to trace things back to the code that generated them:
# config/environments/development.rb
config.active_record.query_log_tags_enabled = true
This will cause your queries to be annotated with helpful context, making debugging a breeze!
N+1 issues: a silent performance killer
N+1 queries sneak in unnoticed and wreak havoc on your app’s performance (kind of like some kind of technologically obsessed anti-Santa who does evil deeds instead of the present thing, …a sort of cyberpunk anti-hero who may or may not be the subject of a pending movie script from one of this article’s authors!)
To catch these pesky N+1 query problems, we recommend using the Prosopite gem. Unlike Bullet, Prosopite watches logs and stacktraces to detect N+1, which makes it less prone to false negatives/positives. Note that there are downsides to this—collecting stack traces is not free, so we don’t recommend turning this on in production:
# config/initializers/prosopite.rb
unless Rails.env.production?
require 'prosopite/middleware/rack'
Rails.configuration.middleware.use(Prosopite::Middleware::Rack)
end
Take a note that this snippet enables tracking only for foreground app code–background jobs, ActionCable channels and other async parts require separate setup. The same considerations apply to non-integration level tests.
Use the database when possible
At times, we end up relying on external tools for tasks that, really, the database itself could handle more efficiently. So, let’s bring some of that functionality back where it shines:
PaperTrail → Logidze: Instead of using Papertrail for versioning, consider Logidze, which stores and manages version history directly in your database. It’s faster, leaner, and keeps everything in one place.
Model Validations → Database Constraints: Complex validations may cause significant performance load. Try to speed them up by leveraging the database when possible. DatabaseValidations provides additional validation helpers to ensure data integrity without relying solely on Rails validations.
Day 9 festive action plan
- Install PgHero to check your database configuration.
- Enable Rails Annotated Query logs to hunt down slow queries.
- Check your app for N+1 issues using Prosopite.
And use the database when possible:
- Consider replacing PaperTrail with Logidze.
- Try using validations at the database level with DatabaseValidations.
Wrap it up like a perfectly optimized query and share your recipes with us using #railsmas!
Day 10: deck the docs with lines of clarity
On the tenth day of Railsmas, we’re tackling documentation. Yes, thinking about documentation during the holidays …sounds fun? Trust us, future you (and your team) will thank you when you’re not frantically trying to remember why you made that one weird design decision back in March.
Check Your README for outdated documentation
Your README is the front door to your project, so try to take a moment to review your README wearing a newbie hat and check if all the instructions are still accurate.
Note: Need to track down all those TODOs, FIXMEs, and “I’ll fix this later” promises? Run bin/rails notes
to get a tidy list of annotations hiding in your code.
Start documenting your API
If your app has an API, the slow season after the holidays is the perfect time to start documenting it (or updating those docs you’ve been neglecting). We highly recommend taking a “documentation-first” approach: write the docs before you write the code. This not only ensures your API is well-documented, it also forces you to think through the design before implementation.
Moving on, even the best API documentation can have inconsistencies, typos, or missing fields. That’s where Spectral comes in; it’s a linter for API documentation that helps you catch issues before they become a problem.
npx @stoplight/spectral lint docs/openapi.yml
And if you’re using GraphQL, it’s time to give your documentation a little love. Missing or outdated descriptions and examples can lead to confusion and frustration, so make sure they’re accurate and reflect the current state of your schema. The RuboCop::GraphQL gem can help you to enforce consistent documentation standards.
And remember, if writing APIs feels like too much work, there’s always Inertia Rails—because sometimes the best API is no API at all.
Day 10 festive action plan
- Check your README as if you were a newbie onboarding on the project.
- If not already done, document at least one of your API’s endpoints and leverage the documentation-first approach.
- Lint your API documentation with Spectral.
- For GraphQL, add RuboCop::GraphQL.
Finally, tired of all those APIs? Consider Inertia Rails next time!
And in the meantime, share your experiences with poorly documented APIs and showcase your own best practices using #railsmas.
Day 11: Jemalocking around the Christmas tree
On the eleventh day of Railsmas: the world of performance tuning and optimization! Because nothing says “holiday cheer” like shaving precious milliseconds off your boot time.
Memory management magic
Let’s face it, Rails “adores” memory and this love definitely affects not only the consumption of resources, it also brings a performance penalty. (And unfortunately, memory management is not free and it’s handled outside of Ruby.)
Thankfully, the Rails community proposed a way to optimize memory allocation years ago: either set a magical MALLOC_ARENA_MAX
environment value, or replace the system allocator completely with Jemalloc.
For those who want a convenient prepacked solution, we vouch for Fullstaq, which also backports malloc_trim
patch for Ruby < 3.3, into a Docker image.
These tweaks are especially useful for multi-threaded apps. That said, while these tricks are widespread, they’re not applied to Ruby by default. Ruby isn’t just for Rails, and not every program benefits from custom memory allocation strategies.
A Docker & Bootsnap BFF story
Rails developers use Bootsnap (caching expensive application loading computation) to speed up boot times during development. …but what if we told you that Bootsnap can speed up your production startup time too?
To get a loading boost in production you might need to precompile the cache:
bundle exec bootsnap precompile --gemfile app/ lib/
Do not forget to include all relevant directories with code to be cached properly (i.e. config
, engines
, packs
and other custom top-level directories). To validate it, you can use simple instrumentation:
# config/boot.rb
# put this at the bottom of the file:
Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
Now, clear and precompile the cache and check Bootsnap instrumentation doesn’t show any misses:
rm -rf tmp/cache/bootsnap/*
bundle exec bootsnap precompile --gemfile app/ lib/
bundle exec rails r 'puts :ok' | grep 'miss'
(Be extra careful with Docker image layers—it’s common to leave precompiled bootstrap cache behind since it’s located in a tmp
directory.)
A bit tedious, but tedium is Santa’s way of doing things; 364 days of preparation for one big night of execution!
The Notorious YJIT
Remember all those experiments with JIT. Spooky, right? In fact, starting last year Rails turned the YJIT on by default, without any special flags. Are you running Rails 7.2 and Ruby 3.3? You’re probably already using YJIT, but there’s a caveat: developers frequently forget to update the load_defaults
method, and if so, you can just turn YJIT on:
# config/application.rb
# Development and test environment tend to reload code and
# redefine methods (e.g. mocking), hence YJIT isn't generally
# faster in these environments.
config.yjit = !Rails.env.local?
This is a safe alternative to a more wreckless approach of updating the load_defaults
value, which should be done with proper attention to every updated config value.
Keep an eye on performance metrics to see if YJIT makes a noticeable difference for your app. And remember, not all workloads will benefit from JIT compilation, so test before you commit!
Sidetrack with frontend and Vite
If you’re optimizing, you can’t forget about the asset pipeline. In particularly, if you’re still relying on a Webpacker-based pipeline, it might be time to trade in that old sleigh for a turbocharged snowmobile: Vite Ruby.
Vite Ruby is a modern frontend toolchain that brings faster builds, live reload, and hot module replacement to Rails. It’s like unwrapping a shiny new gift for your frontend workflow—no more waiting forever for assets to compile.
And if you’re feeling extra daring, check out rolldown-vite
, a temporary fork of Vite powered by the experimental Rolldown bundler written in Rust. It’s faster than Santa’s sleigh on turbo mode:
// package.json
"vite": "npm:rolldown-vite@0.1.0"
While it doesn’t yet support all the configuration options needed for Ruby Vite, it’s a glimpse into the future of even faster asset pipelines; your frontend builds might just be faster than Santa chugging two pints of whole milk. And, for the record, he is able to do this like, deceptivley quickly.
Day 11 festive action plan
- Optimize your Rails memory consumption by setting
MALLOC_ARENA_MAX
- …or even better, by using Jemalloc
- Make sure you’re precompiling Bootsnap cache in production without cache misses
- Try out YJIT (test results before rolling out).
- Consider speeding up your asset pipeline with Vite.
Then, share your speed-up tricks using #railsmas, because nothing says “holiday spirit” like application speed optimization.
Day 12: Silent night, stable night
On the twelfth and final day of Railsmas, we’re wrapping things up with the ultimate gift to your app: stability. Because what good is a fast, secure, and well-documented app if it crashes the moment a user sneezes in its direction? So, grab your warmest blanket, a cup of cocoa, and let’s ensure your app is built to last.
Set timeouts: don’t let requests roast on an open fire
Time is precious, especially during the holidays. Setting proper timeouts for your app ensures that slow or stuck requests don’t hog resources and bring everything to a halt. Without timeouts, unresponsive services can consume critical resources indefinitely, potentially leading to system-wide slowdowns or crashes.
Afraid to miss a potential point of failure? Don’t be!
The Ultimate Guide to Ruby Timeouts provides a comprehensive list of code snippets for every situation where timeouts are critical in your Ruby applications.
There’s at least one timeout your Rails application absolutely must have: the database timeout:
production:
adapter: postgresql
variables:
statement_timeout: 30s
Caution: The value you set for the timeout is critical and should be tailored to your application’s needs. While 30 seconds might work for many apps, others with a lot of long-running queries might require a higher value to avoid prematurely killing legitimate queries.
Sidekiq: the engine driving your winter wonderland
Sidekiq is a workhorse for background jobs, but even workhorses need a little TLC to stay reliable. Let’s share a couple of tips to keep your Sidekiq queues running smoothly.
The most common oversight is failing to realize that Sidekiq can lose jobs during a segfault or other critical crashes. Sidekiq pops job entries from the Redis queue, and there is no acknowledgment mechanism in place by default.
While Sidekiq Pro comes with the super_fetch
feature to ensure jobs don’t get lost if a worker crashes, there’s a OSS alternative from GitLab: sidekiq-reliable-fetch. This gem wraps job fetching in a more robust process, ensuring that even if your worker crashes mid-task, the job will be re-queued for another worker to pick up. It’s like having Santa’s elves double-check the naughty-and-nice list—nothing gets left behind.
Sidekiq is fairly flexible, but when it comes to fair distribution of jobs, one “greedy” client can take over the whole background processing. To fix this, we need Robin Hoo… a round-robin scheduling algorithm.
The Sidekiq::FairTenant gem provides a solution by throttling jobs from resource-heavy clients. It adds a weighted queues feature to re-route jobs that exceed a defined threshold to lower-priority queues. This ensures these jobs don’t block others while still maintaining overall throughput.
ActionCable to AnyCable: no more messages lost in a blizzard!
If you’re using ActionCable for WebSockets, it might be time to consider switching to AnyCable.
Not only does AnyCable offload WebSocket connections to a separate Go server, reducing the load on your Rails app and improving scalability.
It also comes with a special gift: Action Cable Extended protocol which provides several stability improvements, including better handling of connection lifecycle events, and support for more robust reconnection strategies.
Threads and databases
Finding the right number of server processes and threads is a balancing act. And more here does not always mean better, for example, recently Rails core team lowered the default Puma thread count from 5 to 3, since that showed better response times and resource utilization for good behaving Rails applications (without long running queries and sync 3rd party calls).
To find the best suiting number of workers and threads for your application, use the classic Scaling Ruby Apps to 1000 Requests per Minute - A Beginner’s Guide and the brand new Tuning Performance for Deployment Rails guide.
Check your application for sensible configuration values to eliminate errors on peak load, or to save money next year if you’re overprovisioning.
If you still encounter connection timeout errors after proper configuration updates, it’s a nice idea to check that all parts of your application which work with databases are wrapped with Rails Executor / Reloader.
If you’re using gems that are utilizing threading mechanisms, make sure they’re leveraging Rails locking mechanisms to prevent database access timeout errors, for example: Karafka, Sidekiq, AnyCable etc. Some gems expect users to manually manage thread safety: Sucker Punch, Kicks (ex-Sneakers), and so on.
Day 12 festive action plan
- Set timeouts (database, HTTP, etc.).
- Optimize Sidekiq resiliency with sidekiq-reliable-fetch and fair queueing with Sidekiq::FairTenant.
- Consider switching from ActionCable to AnyCable for connection stability improvements.
- Tune number of workers and threads for optimal performance.
- Use Rails Executor/Reloader to fix database connection timeouts.
Share your ways of reaching stability with our stable #railsmas, because nothing says holiday spirit like an app that’s stable enough to survive even the wildest of holiday traffic spikes.
Twas’ the heading at the end of the post
Well, well! Twelve days down! Now, by the time the New Year begins, you’ll have transformed your codebase into something truly worth celebrating. Happy New Year, and welcome 2025!