<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Shivam's Blog</title>
        <link>https://www.shivamchahar.com</link>
        <description>Learn Ruby, Rails, JavaScript, and React with Shivam.</description>
        <lastBuildDate>Fri, 03 Apr 2026 23:29:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for Node.js</generator>
        <language>en</language>
        <image>
            <title>Shivam's Blog</title>
            <url>https://www.shivamchahar.com/favicon.ico</url>
            <link>https://www.shivamchahar.com</link>
        </image>
        <copyright>All rights reserved 2026, Shivam Chahar</copyright>
        <item>
            <title><![CDATA[Rails Performance: 5 Critical Bottlenecks You're Missing]]></title>
            <link>https://www.shivamchahar.com/posts/rails-performance-5-critical-bottlenecks-you-are-missing</link>
            <guid>https://www.shivamchahar.com/posts/rails-performance-5-critical-bottlenecks-you-are-missing</guid>
            <pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Fix slow Rails apps in 5 steps. Guide covers N+1 queries, missing indexes, memory bloat, view rendering & asset compilation with real-world examples.]]></description>
            <content:encoded><![CDATA[
## Why Performance Triage Matters

After profiling many Rails applications in production, performance issues tend to follow predictable patterns. When something is slow, it's rarely a mystery—it's almost always due to one of five common culprits. Here's a practical checklist for diagnosing and fixing Rails performance problems, ordered by impact 🎯.

## ⏱️ Quick Wins First (2-Minute Fixes)

Before diving into the details, here are the three changes that fix 80% of slow Rails app issues:

```ruby
# 1. Catch N+1 queries automatically
gem 'bullet', groups: [:development, :test]

# 2. Load associations together (not separately)
Post.includes(:author)  # Instead of Post.all

# 3. Add missing foreign key indexes
add_index :posts, :user_id
add_index :comments, :post_id
```

**Expected impact**: Response times often improve by 5-10x with just these three changes.

## The Performance Triage Approach

Not all bottlenecks are created equal. Some give you 10x improvements with minimal effort, while others require major refactoring for marginal gains. This list is ordered by **impact-to-effort ratio** based on common production scenarios.

## Bottleneck #1: N+1 Queries (The Silent Killer)

### Why It's #1

N+1 queries are one of the most common performance killers in Rails applications. They're easy to introduce, hard to spot in development (with small datasets), and can significantly impact production performance.

**Real-world impact**: A typical homepage loading 50 posts with N+1 queries makes 51 database queries instead of 2. This can slow response times from 200ms to 2+ seconds—a 10x performance hit.

### How to Spot It

N+1 queries typically show up as slow endpoints, increased database load, or degraded response times. Common tools for detecting them include:

- Bullet gem in development
- APM tools like NewRelic or Scout in production
- Rails query logs with `config.active_record.verbose_query_logs = true`

### The Fix

**Before (N+1):**

```ruby
# In your controller
@posts = Post.all

# In your view - triggers N queries
@posts.each do |post|
  post.author.name  # Loads author for each post!
  post.comments.count  # Loads comments for each post!
end
```

**After (Optimized):**

```ruby
# In your controller
@posts = Post.includes(:author).with_comments_count

# Option 1: Counter cache (best for frequently accessed counts)
# In Comment model (the child)
class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true  # ← counter_cache goes here
end

# Migration to add counter cache column and backfill existing data
class AddCommentsCountToPosts < ActiveRecord::Migration
  def change
    add_column :posts, :comments_count, :integer, default: 0

    reversible do |dir|
      dir.up do
        # Backfill existing records (critical for existing data)
        Post.find_each { |post| Post.reset_counters(post.id, :comments) }
      end
    end
  end
end

# Option 2: Real-time counts without counter cache (if you need calculated counts)
scope :with_comments_count, -> {
  left_joins(:comments)
    .select('posts.*, COUNT(comments.id) as comments_count')
    .group('posts.id')
}
```

### When to Use What

| **Method**    | **Use Case**                                     | **Example**                                                   |
| ------------- | ------------------------------------------------ | ------------------------------------------------------------- |
| `includes`    | 99% of cases (default choice)                    | `Post.includes(:author)`                                      |
| `preload`     | Polymorphic associations, force separate queries | `Post.preload(:comments)`                                     |
| `eager_load`  | Need to add WHERE conditions on associations     | `Post.eager_load(:author).where(authors: { verified: true })` |
| `joins`       | Filter by association without loading it         | `Post.joins(:author).where(authors: { verified: true })`      |
| Counter cache | Frequently accessed counts                       | `has_many :comments, counter_cache: true`                     |

💡 **Pro Tip**: Running Bullet in your test suite with `Bullet.raise = true` can catch N+1s before they reach production. This prevents performance regressions from sneaking into the codebase.

🚨 **Gotcha**: N+1 queries in background jobs are easy to miss because they don't cause user-facing slowdowns, but they can significantly impact job processing capacity and system resources.

---

## Bottleneck #2: Missing Database Indexes

### Why It's #2

Missing indexes are a common performance issue in Rails applications. A single missing index can turn a 10ms query into a multi-second table scan.

**Real-world impact**: A query filtering 100,000 posts by `user_id` without an index can take 800ms. Add the index, and it drops to 8ms—a 100x improvement.

### How to Spot It

Missing indexes typically manifest through:

- Slow query logs (queries > 100ms)
- High database CPU usage
- `EXPLAIN ANALYZE` showing sequential scans on large tables

### Finding the Culprits

```ruby
# In Rails console
ActiveRecord::Base.logger = Logger.new(STDOUT)

# Run your slow action, then check the logs
# Look for queries that scan many rows

# PostgreSQL EXPLAIN - Progressive debugging approach:

# 1. See query plan (estimated costs)
Post.where(published: true).explain

# 2. See actual execution stats (run the query)
Post.where(published: true).explain(:analyze, :buffers)

# Look for:
# - "Seq Scan" on large tables → missing index
# - High "Buffers" numbers → too many rows scanned
# - Actual time >> estimated time → statistics out of date
```

### The Fix

**Common patterns that need indexes:**

```ruby
# Foreign keys (often missed!)
add_index :posts, :user_id
add_index :comments, :post_id

# Boolean flags you filter on
add_index :posts, :published
add_index :users, :admin

# Timestamps you sort/filter by
add_index :posts, :created_at
add_index :posts, :published_at

# Composite indexes for common query combinations
add_index :posts, [:user_id, :published, :created_at]

# Composite index on foreign key + status (very common pattern)
add_index :comments, [:post_id, :approved]
add_index :orders, [:user_id, :status]

# Partial indexes for conditional queries
add_index :posts, :featured, where: "featured = true"
add_index :users, :email, where: "deleted_at IS NULL"
add_index :comments, [:post_id, :approved], where: "approved = true"
```

### Decision Framework

**When to add an index:**

- Foreign keys (always!)
- Columns used in WHERE clauses frequently
- Columns used for sorting (ORDER BY)
- Columns used in JOIN conditions

**When NOT to add:**

- Tables with < 1,000 rows (usually not worth it)
- Columns that are frequently updated (indexes slow writes)
- Columns with very low cardinality (e.g., boolean with 90% one value)
  - **Exception**: Use partial indexes for low-cardinality columns when you only query one value:

```ruby
# Problem: posts.featured is boolean, 90% are false
# Full index is mostly useless since you only query featured = true

# Solution: Partial index (only indexes true values)
add_index :posts, :featured, where: "featured = true"

# Or for common status combinations:
add_index :comments, [:post_id, :approved], where: "approved = true"
add_index :orders, [:user_id, :status], where: "status = 'pending'"
```

💡 **Pro Tip**: Use `EXPLAIN (ANALYZE, BUFFERS)` in PostgreSQL to see actual performance, not just the query plan. It shows real execution time and how many rows were actually scanned.

🚨 **Gotcha**: Indexes speed up reads but slow down writes. Adding too many indexes to a table can negatively impact insert and update performance. Balance is key.

---

## Bottleneck #3: Inefficient View Rendering

### Why It's #3

After optimizing database queries, view rendering often becomes the next bottleneck—especially for pages with lots of partials or complex logic.

### How to Spot It

Signs of view rendering problems include:

- Fast database queries but slow page rendering
- High "View" time in APM tools
- Deeply nested partials

### Common Culprits

**1. Logic in Views**

```erb
<%# BAD - running queries in the view %>
<% @posts.each do |post| %>
  <% if post.comments.where(approved: true).any? %>
    <%# ... %>
  <% end %>
<% end %>

<%# GOOD - precompute in controller/model %>
<% @posts.each do |post| %>
  <% if post.has_approved_comments? %>
    <%# ... %>
  <% end %>
<% end %>
```

**2. Too Many Partials**

```erb
<%# This renders 100 partials (slow!) %>
<% @posts.each do |post| %>
  <%= render "post_card", post: post %>
<% end %>

<%# Better - use collection rendering %>
<%= render partial: "post_card", collection: @posts, as: :post %>
```

**3. Unnecessary JSON Serialization**

```erb
<%# BAD - using to_json in views %>
<%= @posts.to_json %>

<%# GOOD - use a proper serializer %>
<%= render json: @posts, each_serializer: PostSerializer %>
```

### The Fix: Fragment Caching

Fragment caching can significantly improve view rendering performance by storing rendered HTML fragments:

```erb
<%# Cache the expensive part %>
<% cache @post do %>
  <%= render "post_content", post: @post %>
<% end %>

<%# Russian doll caching with proper invalidation %>
<% cache @post do %>
  <%= render @post %>

  <% cache [@post, @post.comments.cache_key_with_version] do %>
    <%= render @post.comments %>
  <% end %>
<% end %>

<%# Collection caching %>
<%= render partial: "post_card", collection: @posts, cached: true %>
```

For proper cache invalidation with Russian doll caching, ensure child records touch their parent:

```ruby
class Comment < ApplicationRecord
  belongs_to :post, touch: true  # Updates post.updated_at when comment changes
end
```

The `cache_key_with_version` method ensures the cache key updates when any comment changes, providing automatic cache invalidation.

💡 **Pro Tip**: Use Russian doll caching for nested content where inner caches can be reused when outer fragments change. The `touch: true` option ensures parent caches invalidate when child records change, maintaining cache consistency.

---

## Bottleneck #4: Memory Bloat in Background Jobs

### Why It's #4

Background jobs are often overlooked in performance optimization, but they're critical for overall system health. Memory-intensive jobs can impact your entire job processing infrastructure.

**Real-world impact**: A job processing 10,000 users with `User.all.each` can consume 2GB+ of RAM and crash workers. Using `find_each` keeps memory constant at ~50MB regardless of dataset size.

### How to Spot It

Warning signs of memory issues in background jobs:

- Sidekiq/job workers using excessive RAM
- Workers getting OOM-killed
- Job processing slowing down over time

### Common Causes

**1. Loading Too Much Data**

```ruby
# BAD - loads all records into memory
User.all.each do |user|
  UserMailer.weekly_digest(user).deliver_now
end

# GOOD - batching
User.find_each(batch_size: 100) do |user|
  UserMailer.weekly_digest(user).deliver_now
end
```

**2. Holding References**

```ruby
# BAD - accumulates all results
results = []
User.find_each do |user|
  results << process_user(user)
end
results.each { |r| do_something(r) }

# GOOD - process and discard
User.find_each do |user|
  result = process_user(user)
  do_something(result)
  # result goes out of scope and can be GC'd
end
```

**3. Not Using Database Operations**

```ruby
# BAD - loads all records into memory
Post.where(published: false).each(&:destroy)

# GOOD - uses SQL (no callbacks, just SQL DELETE)
Post.where(published: false).delete_all

# If callbacks ARE needed, batch it:
Post.where(published: false).find_each do |post|
  post.destroy  # Calls callbacks but processes in batches
end
```

💡 **Pro Tip**: Use `find_each` with appropriate batch sizes for processing large datasets in background jobs. This ensures memory usage stays constant regardless of the total number of records.

🚨 **Gotcha**: Be careful with accumulating results in memory during batch processing. If you need to collect results, consider writing them to a file or database incrementally rather than keeping everything in memory.

---

## Bottleneck #5: Slow Asset Compilation

### Why It's #5

While less critical in production (since assets are precompiled), slow asset compilation can impact developer productivity and CI/CD pipeline speed.

### How to Spot It

Common signs of asset compilation issues:

- Long deployment times
- Slow CI builds
- Slow development environment startup

### The Fix

**1. Use Modern JavaScript Bundlers**

```bash
# esbuild - significantly faster JavaScript bundling
bin/rails javascript:install:esbuild

# Or Vite - similar speed with better DX
```

Modern bundlers provide substantial improvements:

- **esbuild**: ~50% faster JavaScript/TypeScript compilation than Webpacker
- **Vite**: Similar JS/TS speed to esbuild, with hot module replacement
- **Important**: Speed improvements are primarily for JS/TS bundling; CSS compilation improvements are less dramatic
- Main impact: CI/CD pipelines, development startup, and deploy times

**2. Optimize Images**

```ruby
# Use WebP, lazy loading, proper sizes
<%= image_tag "hero.jpg", loading: "lazy", sizes: "100vw" %>
```

**3. CDN for Assets**
Using a CDN for assets can significantly reduce load times by serving static files from geographically distributed servers closer to users.

💡 **Pro Tip**: While asset compilation is precompiled in production, slow builds impact developer productivity and CI/CD speed. Modern bundlers like esbuild primarily speed up JavaScript/TypeScript bundling—CSS compilation improvements are less dramatic.

---
## Wrapping Up

Rails performance optimization isn't magic—it's methodical. Most applications see dramatic speed gains without major rewrites: fix N+1 queries, add the right indexes, and cache what's costly. Start by measuring, target the changes with the biggest impact, and monitor for regressions over time.

**Key takeaways:**
1. **Always measure first** – Let real data guide what you work on.
2. **Prioritize database issues** – Slow queries are almost always the top culprit.
3. **Don't chase micro-optimizations** – Focus on fixes with the highest leverage.
4. **Monitor continuously** – Regular profiling prevents future slowdowns.

Often, just a couple of smart changes can transform how fast your app feels—for you and your users.

---

**Resources:**

**Development:** [Bullet](https://github.com/flyerhzm/bullet?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources) • [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources)

**Production:** [NewRelic](https://newrelic.com/ruby?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources) • [Scout APM](https://scoutapm.com/?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources) • [PgHero](https://github.com/ankane/pghero?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources) • [Skylight](https://www.skylight.io/?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources)

**Docs:** [Rails Query Guide](https://guides.rubyonrails.org/active_record_querying.html?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources) • [PostgreSQL EXPLAIN](https://www.postgresql.org/docs/current/sql-explain.html?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=resources)

---

## Enjoyed this post? Let's Not Make It One-Sided

I’m always up for a chat about code, MVP launches, or wild performance wins—reach out on [X](https://x.com/shivam_s_chahar?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=cta) or [LinkedIn](https://www.linkedin.com/in/shivamchahar/?utm_source=shivamchahar.com&utm_medium=blog&utm_campaign=rails-performance-bottlenecks&utm_content=cta), or just drop your story in the comments below.

Got a tip or a pain point you think the Rails community should hear? Share it below—your experience helps everyone.

More performance insights, code walkthroughs, and deep-dives on Rails bottlenecks coming soon!
]]></content:encoded>
            <author>work@shivamchahar.com (Shivam Chahar)</author>
        </item>
        <item>
            <title><![CDATA[Rails 8.1: Resilient Jobs, Better Logs, and Local CI]]></title>
            <link>https://www.shivamchahar.com/posts/rails-8-1-resilient-jobs-better-logs-local-ci</link>
            <guid>https://www.shivamchahar.com/posts/rails-8-1-resilient-jobs-better-logs-local-ci</guid>
            <pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Rails 8.1 solves real production problems: jobs that resume after restarts, searchable structured logs, and fast local CI. Here's what's worth upgrading for.]]></description>
            <content:encoded><![CDATA[
Rails 8.1 is here, and after digging into the new features, it looks like a solid upgrade 🎯. This release focuses on developer experience and production reliability—job continuations, structured logging, and local CI are the standouts.

Let me walk you through what's new, what's worth your time, and what to watch out for.

## 1. Active Job Continuations: Finally, Resilient Background Jobs

### The Problem

We've all been there: you deploy, and suddenly your long-running import job fails halfway through. Sidekiq restarts, the job starts from scratch, and your users wait another 20 minutes. Writing custom checkpointing logic for these situations is tedious and error-prone.

### The Solution

Rails 8.1 introduces **Active Job Continuations**, which allow jobs to be broken into discrete steps that can resume after a restart. Think of it like savepoints in a video game—your job can pick up right where it left off.

The `step` method comes in three flavors:

```ruby
class ProcessImportJob < ApplicationJob
  include ActiveJob::Continuable

  def perform(import_id)
    @import = Import.find(import_id)

    # 1. Block format - inline logic
    step :initialize do
      @import.update!(status: "processing")
      @import.download_from_s3
    end

    # 2. Block with cursor - for iterating through large datasets
    step :process do |step|
      @import.records.find_each(start: step.cursor) do |record|
        record.process!
        # Save progress after each record
        step.advance! from: record.id
      end
    end

    # 3. Method reference - cleanest for extracted logic
    step :finalize
  end

  private

  def finalize
    @import.update!(
      status: "completed",
      processed_at: Time.current
    )
    ImportMailer.completion_email(@import).deliver_later
  end
end
```

The magic is in that second format with `step.cursor` and `step.advance!`. When your job processes 5,000 out of 10,000 records and gets interrupted, it resumes at record 5,001 instead of starting over. The cursor is automatically saved and restored.

💡 **Pro Tip**: Use the cursor format for any step that iterates through large datasets. This would be perfect for CSV import jobs or any batch processing that deals with large collections.

🚨 **Gotcha**: Steps should still be idempotent (safe to run multiple times) when possible. While the cursor helps, network issues or database locks can cause partial processing within a step. Design your steps to handle being run twice safely.

## 2. Structured Event Reporting: Logs That Actually Help Debug

### Why It Matters

If you've ever tried to parse Rails logs in production, you know the pain. Plain text logs are great for development, but searching, filtering, and monitoring structured data becomes difficult when everything is a string.

### The New Way

Rails 8.1 introduces a unified **Event Reporter** with `Rails.event.notify` for structured, machine-readable logging. Instead of interpolating strings, you emit events with structured data:

```ruby
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)

    if @user.save
      # Old way: Rails.logger.info "User #{@user.id} signed up"

      # New way: structured and searchable
      Rails.event.notify("user.signup",
        user_id: @user.id,
        email: @user.email,
        plan: @user.plan
      )
      # Emits event with:
      # - name: "user.signup"
      # - payload: { user_id: 123, email: "...", plan: "..." }
      # - timestamp (nanoseconds)
      # - source_location (file, line, method)
      # - tags and context (if set)

      redirect_to dashboard_path
    else
      render :new, status: :unprocessable_entity
    end
  end
end
```

You can also add **tags** to categorize events:

```ruby
Rails.event.tagged("graphql") do
  # All events inside this block will include tags: { graphql: true }
  Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")
end
```

And set **context** that persists across events (perfect for request IDs and tenant IDs):

```ruby
class ApplicationController < ActionController::Base
  before_action :set_event_context

  private

  def set_event_context
    Rails.event.set_context(
      request_id: request.request_id,
      shop_id: current_shop&.id
    )
  end
end

# Now all events automatically include request_id and shop_id!
```

To control how events are emitted, you register custom subscribers. Subscribers must implement an `emit` method that receives the event hash:

```ruby
# config/initializers/event_subscribers.rb
class DatadogSubscriber
  def emit(event)
    # event is a hash with:
    # - :name (String)
    # - :payload (Hash with symbolized keys)
    # - :tags (Hash)
    # - :context (Hash)
    # - :timestamp (nanoseconds)
    # - :source_location (Hash with :filepath, :lineno, :label)
    
    Datadog::Statsd.event(
      event[:name],
      event[:payload].to_json,
      tags: event[:tags]
    )
  end
end

Rails.event.subscribe(DatadogSubscriber.new)
```

💡 **Pro Tip**: Set up event context in your ApplicationController for request_id and user_id. Then every event you emit will automatically include that context. This makes debugging production issues so much easier—you can trace an entire request flow across services.

🚨 **Gotcha**: Don't go overboard with events. Focus on business-critical actions (signups, purchases, errors) not every line of code. Too many events = noise = useless logs.

## 3. Local CI: Run Your Full Pipeline Locally

### Why It Matters

One of my biggest frustrations has been waiting 10 minutes for GitHub Actions to tell me I have a failing test. Then I fix it, push again, and wait another 10 minutes to discover I broke something else. Rails 8.1 adds a CI DSL that lets you define your entire CI pipeline in `config/ci.rb` and run it locally with `bin/ci`—catching issues before you even push.

The Rails team mentions that HEY's test suite of 30,000+ assertions runs in just 1m 23s on a Framework Desktop AMD Linux machine and 2m 22s on an M4 Max. That's faster than waiting for cloud CI to even start!

### Setting It Up

```ruby
# config/ci.rb
CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap audit", "bin/importmap audit"
  step "Security: Brakeman", "bin/brakeman --quiet --no-pager"

  step "Tests: Rails", "bin/rails test"
  step "Tests: System", "bin/rails test:system"
  step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"

  # Optional: Integrate with GitHub CLI for PR signoffs
  if success?
    step "Signoff: Ready for merge", "gh signoff"
  else
    failure "Signoff: CI failed. Fix and try again.", "gh signoff"
  end
end
```

Now run your entire CI pipeline locally:

```bash
$ bin/ci
```

No more "fix tests" commits. Catch everything before pushing.

💡 **Pro Tip**: The `gh signoff` integration is clever—it requires a passing local CI run before PRs can be merged. Combined with branch protection rules, this ensures nobody merges broken code.

🚨 **Gotcha**: This works best with fast test suites. If your tests take 20+ minutes, developers will skip `bin/ci`. Invest in test speed first, then enforce local CI.

## 4. Association Deprecations: Refactoring Legacy Code Safely

### The Problem

If you've inherited a legacy Rails app (who hasn't?), you know the pain of removing old associations. Is this `has_many` still used? If I delete it, what breaks? Grepping for the association name doesn't catch all the indirect usage through preloading, nested attributes, or other associations.

### The Solution

Rails 8.1 adds a `deprecated:` option for associations that tracks all usage patterns:

```ruby
class Author < ApplicationRecord
  has_many :posts, deprecated: true
end
```

Now when code calls `author.posts` anywhere—directly, through preloading, nested attributes, or any other method—you get a deprecation warning:

```
DEPRECATION WARNING: posts association is deprecated
```

Rails supports three reporting modes:

```ruby
# config/environments/development.rb
config.active_record.deprecated_associations = :warn   # default, logs warning
config.active_record.deprecated_associations = :raise  # raises exception
config.active_record.deprecated_associations = :notify # sends to ActiveSupport::Notifications
```

The clever part? Rails tracks **all** usage, not just direct calls. This includes:

- `author.posts` (explicit)
- `author.preload(:posts)` (query optimization)
- Nested attributes
- Association callbacks
- Indirect usage through other associations

💡 **Pro Tip**: Use `:raise` mode in your test suite to catch all deprecated usage immediately. Use `:warn` in staging to log production-like usage patterns. Then confidently remove the association when warnings stop appearing. This would have saved me weeks of refactoring time on legacy codebases.

## 🎯 Quick Hits: More Rails 8.1 Wins

Beyond the major features, Rails 8.1 brings several smaller improvements worth mentioning:

**Native Markdown Rendering**: Rails now handles Markdown natively through Action View. If you're building a blog or documentation site, you can use `render markdown: @object` without external gems. It's basic but covers common use cases.

**Kamal Improvements**: If you're using Kamal for deployments, there's a new `rails credentials:fetch` command for pulling secrets from encrypted credentials, and Kamal 2.8 now supports registry-free deployments for simple setups (though you'll still want a remote registry for production).

**Verbose Redirects in Development**: New Rails 8.1 apps have verbose redirect logging enabled by default (`config.action_dispatch.verbose_redirect_logs = true`), making it easier to track redirect chains during development. Add this to your `config/development.rb` if upgrading an existing app.

**Alphabetically Sorted Schema**: The `schema.rb` file now sorts table columns alphabetically, making diffs cleaner and more predictable when reviewing changes.

## Should You Upgrade?

**Upgrade if:**

- ✅ You have long-running background jobs
- ✅ You need structured production logs
- ✅ You want local CI with `bin/ci`
- ✅ You're on Ruby 3.3+

**Wait if:**

- ⏸️ You're on Ruby < 3.3 (upgrade Ruby first)
- ⏸️ Critical gems aren't Rails 8.1 compatible yet
- ⏸️ You're in a code freeze
- ⏸️ Current version works fine and you're not hitting pain points

## Wrapping Up

Rails 8.1 brings useful improvements for job reliability, production debugging, and developer workflow. The standout features:

- 🎯 **Active Job Continuations** – jobs resume from checkpoints
- 📊 **Structured Event Reporting** – machine-readable logs
- 🚀 **Local CI** – catch failures before pushing
- 🔧 **Association Deprecations** – track usage before removal

For most teams on Rails 8.0 or 7.x with Ruby 3.3+, this is a straightforward upgrade.

**Have you upgraded to Rails 8.1 yet?** I'm curious about your experience. Connect on [X](https://x.com/shivam_s_chahar) or [LinkedIn](https://www.linkedin.com/in/shivamschahar/)!

**Resources**:

- [Rails 8.1 Official Announcement](https://rubyonrails.org/2025/10/22/rails-8-1)
- [Rails 8.1 Release Notes](https://guides.rubyonrails.org/8_1_release_notes.html)
- [Rails 8.1 GitHub Release](https://github.com/rails/rails/releases/tag/v8.1.0)
- [Rails 8.1 Commits](https://github.com/rails/rails/compare/@%7B2025-10-17%7D...main@%7B2025-10-24%7D)
- [Upgrade Guide](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html)
- [Rails Guides](https://guides.rubyonrails.org/)
]]></content:encoded>
            <author>work@shivamchahar.com (Shivam Chahar)</author>
        </item>
        <item>
            <title><![CDATA[Rails 7.1 adds support for responsive images]]></title>
            <link>https://www.shivamchahar.com/posts/rails-7.1-adds-responsive-images</link>
            <guid>https://www.shivamchahar.com/posts/rails-7.1-adds-responsive-images</guid>
            <pubDate>Thu, 18 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Rails 7.1 adds picture_tag, which is a more "robust" version of the image_tag. It supports resolution switching, art direction, and fallback image.]]></description>
            <content:encoded><![CDATA[
Rails 7.1 has added support for `picture_tag`. It is useful if we want to add responsive images to our rails app.

## Before

Before, If we wanted to add an image to our app, we would use `image_tag` which supports resolution switching but misses out on art direction, and fallback image.

## After

In Rails 7.1, we can use `picture_tag` for responsive images. `picture_tag` supports:
1. Resolution switching
2. Art Direction
3. Fallback Image

## Usage
### Basic

```erb
<%= picture_tag("image.webp") %>
```

Which will generate the following HTML markup:
```html
<picture>
  <img src="image.webp" />
</picture>
```

### Art Direction
```erb
<%= picture_tag(
    "image-large.jpg",
    sources: [
      { srcset: "image-square.jpg", media: "(max-width: 600px)" },
      { srcset: "image-rectangle.jpg", media: "(max-width: 1023px)" },
      { srcset: "image-large.jpg", media: "(min-width: 1024px)" }
    ])
%>
```

Generates:
```html
<picture>
  <source media="(max-width: 600px)" srcset="image-square.jpg">
  <source media="(max-width: 1023px)" srcset="image-rectangle.jpg">
  <source media="(min-width: 1024px)" srcset="image-large.jpg">
  <img src="image-large.jpg">
</picture>
```

### Block

Full control via block:
```erb
<%= picture_tag do %>
  <%= tag(:source, srcset: image_path("image.webp")) %>
  <%= tag(:source, srcset: image_path("image.png")) %>
  <%= image_tag("image.png") %>
<% end %>
```

Generates:
```html
<picture>
  <source srcset="image.webp" />
  <source srcset="image.png" />
  <img src="image.png" />
</picture>
```

## References
- [Pull Request](https://github.com/rails/rails/pull/48100)
- [The Picture element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture)
- [Responsive Images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)
]]></content:encoded>
            <author>work@shivamchahar.com (Shivam Chahar)</author>
        </item>
        <item>
            <title><![CDATA[Measuring and Optimizing Performance of a Rails application]]></title>
            <link>https://www.shivamchahar.com/posts/measuring-and-optimizing-perf-of-rails-app</link>
            <guid>https://www.shivamchahar.com/posts/measuring-and-optimizing-perf-of-rails-app</guid>
            <pubDate>Fri, 04 Mar 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[A comparative analysis of the performance metrics before and after completing the scale tests. ]]></description>
            <author>work@shivamchahar.com (Shivam Chahar)</author>
        </item>
        <item>
            <title><![CDATA[Navigation in React Router 6]]></title>
            <link>https://www.shivamchahar.com/posts/navigation-in-react-router-6</link>
            <guid>https://www.shivamchahar.com/posts/navigation-in-react-router-6</guid>
            <pubDate>Thu, 23 Dec 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[React Router provides us with an easy-to-use interface for navigation, allowing us to manipulate and subscribe to the browser's history stack.]]></description>
            <content:encoded><![CDATA[
While there are different libraries available for client-side routing, **React Router** is almost always the default choice.

## Why React Router?
As the user navigates, the browser keeps track of each location in a stack. That is how the back and forward buttons work.

For example, consider the user:
  1. Clicks a link to `/blog`
  2. Clicks a link to `/categories`
  3. Clicks the back button
  4. Clicks a link to `/contact`


The history stack will change as follows, where the highlighted entries denote the current URL.
  1. `/blog`
  2. `/blog`, `/categories`
  3. `/blog`, `/categories`
  4. `/blog`, `/contact`

If we click and hold the back button in a browser, we can see the browser’s history stack right there.

Now, some of us might argue that we don’t necessarily need a library to manipulate the history stack. We can do that programmatically like so:

```jsx
<a
  href="/blog"
  onClick={event => {
    // stop the browser from changing the URL
    event.preventDefault();
    // push an entry into the browser history stack and change the URL
    window.history.pushState({}, undefined, "/blog");
  }}
/>
```
While the above code changes the URL. It doesn’t do anything about the UI. We will still need to subscribe to the changes in the history stack to show the correct UI on `/blog`.

[Read more about browser’s History API](https://developer.mozilla.org/en-US/docs/Web/API/History)

React Router makes it easier for us to subscribe to the changes in the browser’s history stack. It also allows us to manipulate the history stack.

## Navigation
React Router provides us with an easy-to-use interface for navigation.

We can use:
  1. `<Link>` and `<NavLink>`, which renders an `<a>` element. We can initiate navigation by clicking on it.
  2. `useNavigate` and `<Navigate>` which will enable us to navigate programmatically.

Let us look at `<Link>` and `<NavLink>` and their usage.
```jsx
import { Link } from "react-router-dom";

function Navbar() {
  return (
    <nav>
      <Link to="blog">Blog</Link>
      <Link to="categories">Categories</Link>
      <Link to="contact">Contact</Link>
    </nav>
  )
}
```
We can use `<NavLink>` in the above example instead of `<Link>`.

The difference between the two is that `<NavLink>` knows whether or not it is “active”. So if we want to apply some styles to the active link, we need to use `<NavLink>`.

[Read more about NavLink](https://reactrouter.com/docs/en/v6/api#navlink)

Now, consider a scenario where we want to navigate our users to `/dashboard` after they successfully log in. This is exactly the place where we would want to navigate programmatically.

React Router provides us `useNavigate` and `<Navigate>` to do exactly that.

Let us see how we can use them:
```jsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

function LoginForm() {
  const [user, setUser] = useState(null);
  const [error, setError] = userState(null);
  const navigate = useNavigate();

  const handleSubmit = event => {
    event.preventDefault();
    try {
      const user = await login(event.target);
      setUser(user);
      navigate("/dashboard", { replace: true });
    } catch (error) {
      setError(error);
    }
  }

  return (
    <div>
      {error && <p>{error.message}</p>}
      <form onSubmit={handleSubmit}>
        <input type="text" name="username" />
        <input type="password" name="password" />
        <button type="submit">Login</button>
      </form>
    </div>
  );
}
```

Alternatively, we can also use `<Navigate>` like this:
```jsx
import React, { useState } from "react";
import { Navigate } from "react-router-dom";

function LoginForm() {
  const [user, setUser] = useState(null);
  const [error, setError] = userState(null);

  const handleSubmit = event => {
    event.preventDefault();
    try {
      const user = await login(event.target);
      setUser(user);
    } catch (error) {
      setError(error);
    }
  }

  return (
    <div>
      {error && <p>{error.message}</p>}
      {user && (
        <Navigate to="/dashboard" replace={true} />
      )}
      <form onSubmit={handleSubmit}>
        <input type="text" name="username" />
        <input type="password" name="password" />
        <button type="submit">Login</button>
      </form>
    </div>
  );
}
```
With this, we do not have to worry about manipulating the history stack and subscribing to its changes. React Router handles all of that for us!

React Router 6 provides a few low-level APIs that can be useful while building our navigation interfaces -
- [useResolvedPath](https://reactrouter.com/docs/en/v6/api#useresolvedpath)
- [useHref](https://reactrouter.com/docs/en/v6/api#usehref)
- [useLocation](https://reactrouter.com/docs/en/v6/api#uselocation)
- [useLinkClickHandler](https://reactrouter.com/docs/en/v6/api#uselinkclickhandler)
- [useLinkPressHandler](https://reactrouter.com/docs/en/v6/api#uselinkpresshandler)
- [resolvePath](https://reactrouter.com/docs/en/v6/api#resolvepath)

Check out the [React Router 6 API doc to learn more](https://reactrouter.com/docs/en/v6/api#navigation)
]]></content:encoded>
            <author>work@shivamchahar.com (Shivam Chahar)</author>
        </item>
        <item>
            <title><![CDATA[What's new in React Router 6?]]></title>
            <link>https://www.shivamchahar.com/posts/whats-new-in-react-router-6</link>
            <guid>https://www.shivamchahar.com/posts/whats-new-in-react-router-6</guid>
            <pubDate>Thu, 02 Dec 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[React Router 6 has some amazing features added with improved bundle size.]]></description>
            <content:encoded><![CDATA[
React creates single-page applications. In single-page applications, it is important to display multiple views without having to reload the browser. React Router plays an important role in managing this. It is the most popular lightweight, fully-featured routing library for React.

The latest release of React Router 6, has created a lot of buzz in the React community.

In this blog, we will look into some of the new changes in React Router 6.

## More compact and elegant

React Router v6 was built from scratch using React hooks. It helped the v6 code to be more compact and elegant than the v5 code.

## Smaller bundle size

In v6 the minified gzipped bundle size dropped by more than 50%. React Router now adds less than 4kb to our total app bundle. With tree shaking enabled, the actual results will be even smaller.

## Routes replaces Switch

`<Switch>` is replaced with `<Routes>`. Routes component has a new prop called `element`, where we can pass the component it needs to render. Instead of scanning the routes in order, `<Routes>` automatically picks the best one for the current URL.

Let us take the example of Blogs.

```jsx
// v5
<Switch>
  <Route path="blogs/:slug" component={Blog} />
  <Route path="blogs/new" component={NewBlog} />
</Switch>
```

The way these routes are ordered matters a lot if we are using `Switch`. The Route element with param `slug` will also catch the `/blog/new` route and, the `Blog` component will be rendered instead of `NewBlog`. We can probably fix this issue by adding `exact` to the `<Route>`.

Now let’s modify the same example to use `Routes`.

```jsx
// v6
<Routes>
  <Route path="blogs/:slug" element={<Blog />} />
  <Route path="blogs/new" element={<NewBlog />} />
</Routes>
```

With this change, we don’t need to worry about the order anymore because `Routes` picks the most specific routes first based on the current URL.

## Relative Links

Earlier, we had to specify the entire path in the `Link`. But now, the path of a `Link` is relative to the path of `Route` that rendered it.

```jsx
import {
  Routes,
  Route,
  Link,
  Outlet
} from "react-router-dom";

function Blogs() {
  return (
    <div>
      <h1>Blogs</h1>
      <nav>
        <Link to="new">New Blog</Link>
      </nav>
      <hr />
      <Outlet />
    </div>
  );
}

function Blog() {
  return <h1>Blog</h1>;
}

function NewBlog() {
  return <h1>New Blog</h1>;
}

function App() {
  return (
    <Routes>
      <Route path="blogs" element={<Blogs />}>
        <Route path=":slug" element={<Blog />} />
        <Route path="new" element={<NewBlog />} />
      </Route>
    </Routes>
  );
}
```
The `Link` in the above example will link to `/blogs/new` because it’s rendered inside `<Blogs>`. Also, we don’t need to change the path if we ever change the parent `URL`.

Keep in mind that Relative `<Link to>` values do not begin with `/`.

## Nested Routes

With React Router 6, we can now nest our Routes inside one another, and their paths will nest too.

```jsx
function App() {
  return (
    <Routes>
      <Route path="blogs" element={<Blogs />}>
        <Route path=":slug" element={<Blog />} />
        <Route path="new" element={<NewBlog />} />
      </Route>
    </Routes>
  );
}
```

It makes our layout code more manageable.

## Index Routes
Index route is a child route with no path that renders in the parent’s [outlet](https://reactrouter.com/en/6.11.1/start/concepts#outlet) at the parent’s [URL](https://reactrouter.com/en/6.11.1/start/concepts#url).

Another way to think of index routes is that it’s the default child route when the parent matches but none of its children do. Mostly we might not need an index route. But, if there is any sort of persistent navigation in the parent route we’ll most likely want index route to fill the space when the user hasn’t clicked one of the items yet.

```jsx
function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Blogs />} />
        <Route path="about" element={<About />} />
      </Route>
    </Routes>
  );
}
```
This allows the `Blogs` to render by default at `/`.

### Note
- Absolute paths still work in v6 to help upgrade easier
- We can ignore relative paths altogether and keep using absolute paths forever if we would like to.

Check out more details about all the cool stuff that has been added to [React Router on the official blog](https://remix.run/blog/react-router-v6)
]]></content:encoded>
            <author>work@shivamchahar.com (Shivam Chahar)</author>
        </item>
    </channel>
</rss>