<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: A.Talaat</title>
    <description>The latest articles on DEV Community by A.Talaat (@a-talaat-aly).</description>
    <link>https://dev.to/a-talaat-aly</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F508595%2F8c78c213-19d9-4e59-964d-686fa80352be.jpeg</url>
      <title>DEV Community: A.Talaat</title>
      <link>https://dev.to/a-talaat-aly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/a-talaat-aly"/>
    <language>en</language>
    <item>
      <title>Building a Rails Notification System That Survives Scale</title>
      <dc:creator>A.Talaat</dc:creator>
      <pubDate>Mon, 31 Aug 2026 22:14:43 +0000</pubDate>
      <link>https://dev.to/a-talaat-aly/building-a-rails-notification-system-that-survives-scale-8h8</link>
      <guid>https://dev.to/a-talaat-aly/building-a-rails-notification-system-that-survives-scale-8h8</guid>
      <description>&lt;p&gt;Every Rails app grows a notification system eventually. &lt;br&gt;
It usually starts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;after_create&lt;/span&gt; &lt;span class="ss"&gt;:notify_watchers&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_watchers&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;NotificationMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
      &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"New comment on &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works beautifully — until a post has 40,000 watchers and someone comments. Now you have 40,000 inserts and 40,000 mailer jobs inside a request cycle, a timeout, a half-written notification table, and an on-call ping.&lt;/p&gt;

&lt;p&gt;The fix isn't a gem. It's four architectural decisions. Let's walk through them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate the event from the delivery
&lt;/h2&gt;

&lt;p&gt;The single most important split: &lt;strong&gt;what happened&lt;/strong&gt; is not &lt;strong&gt;who gets told&lt;/strong&gt; and is not &lt;strong&gt;how they get told&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event (comment.created)
  └─&amp;gt; Fan-out (who should know?)
        └─&amp;gt; Notification records (in-app inbox)
              └─&amp;gt; Deliveries (email, push, Slack, SMS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate concerns, three separate failure domains. If Firebase is down, your in-app inbox still works. If fan-out is slow, the comment still saved.&lt;/p&gt;

&lt;p&gt;Model it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# events: what happened, once&lt;/span&gt;
&lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;            &lt;span class="c1"&gt;# "comment.created"&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt;   &lt;span class="ss"&gt;:payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# notifications: one row per recipient&lt;/span&gt;
&lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;   &lt;span class="ss"&gt;:kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt;    &lt;span class="ss"&gt;:payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;   &lt;span class="ss"&gt;:idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="ss"&gt;:read_at&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That unique index is not decoration — it's what makes the whole pipeline safely retryable. More on that in a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Never fan out in the request
&lt;/h2&gt;

&lt;p&gt;The web request creates the event and enqueues exactly one job. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="ss"&gt;:publish_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on: :create&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_event&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;kind: &lt;/span&gt;&lt;span class="s2"&gt;"comment.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;actor: &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;payload: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;comment_id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post_id: &lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FanOutJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;after_commit&lt;/code&gt;, not &lt;code&gt;after_create&lt;/code&gt;. Enqueueing inside a transaction is the classic race: the job starts, queries for a record the DB hasn't committed yet, and dies with &lt;code&gt;RecordNotFound&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fan-out job then resolves the audience and &lt;strong&gt;chunks it&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications::FanOutJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;
  &lt;span class="no"&gt;BATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1_000&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Audience&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find_in_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;batch_size: &lt;/span&gt;&lt;span class="no"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WriteBatchJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One long job becomes N short jobs. Short jobs retry cheaply, spread across workers, and don't hold a database connection for four minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bulk insert, don't loop
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create!&lt;/code&gt; in a loop is 40,000 round trips. &lt;code&gt;insert_all&lt;/code&gt; is 40 round trips at batch size 1,000.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications::WriteBatchJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recipient_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;recipient_id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;event_id: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;kind: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;payload: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;idempotency_key: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="no"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique_by: :index_notifications_on_idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeliverBatchJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unique_by&lt;/code&gt; turns this into an upsert-style no-op on conflict. Now if Sidekiq retries this job after a network blip, you don't double-notify anyone. &lt;strong&gt;Idempotency is what lets you retry aggressively, and retrying aggressively is what makes the system reliable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You skip callbacks and validations with &lt;code&gt;insert_all&lt;/code&gt; — that's the trade. Notifications are derived data with a fixed shape, so it's a trade worth making. Just keep the shape enforced in one place (the row builder above).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make channels pluggable and preference-aware
&lt;/h2&gt;

&lt;p&gt;Delivery is where the fan-out multiplies again: one notification, three channels. Keep each channel a small object with the same interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Notifications&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Channels&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Email&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;NotificationMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;notification: &lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver_now&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Push&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;PushClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;token: &lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;device_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;REGISTRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="no"&gt;Channels&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;push: &lt;/span&gt;&lt;span class="no"&gt;Channels&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Push&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;slack: &lt;/span&gt;&lt;span class="no"&gt;Channels&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Slack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then resolve preferences once, in bulk, instead of per-user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications::DeliverBatchJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;event_id: &lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;recipient_id: &lt;/span&gt;&lt;span class="n"&gt;recipient_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:recipient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat_map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Preferences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channels_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeliverJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_all_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeliverJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;perform_all_later&lt;/code&gt; (Rails 7.1+) pushes the whole batch to Redis in one call instead of one round trip per job. On Sidekiq you can also reach for &lt;code&gt;Sidekiq::Client.push_bulk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each leaf job handles exactly one delivery, so failures are isolated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications::DeliverJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;retry_on&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OpenTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Errno&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ECONNREFUSED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="ss"&gt;wait: :polynomially_longer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attempts: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="n"&gt;discard_on&lt;/span&gt; &lt;span class="no"&gt;ActiveJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeserializationError&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;REGISTRY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give email, push, and in-app their own queues with their own concurrency. A backed-up SMS provider should never starve your push notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unread badge problem
&lt;/h2&gt;

&lt;p&gt;Here's the query that quietly kills you at scale — it runs on every single page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;read_at: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fixes, use both:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A partial index&lt;/strong&gt;, so Postgres only indexes rows you actually query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:recipient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;where: &lt;/span&gt;&lt;span class="s2"&gt;"read_at IS NULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"index_unread_notifications"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A read watermark&lt;/strong&gt;, so "mark all as read" is one write instead of 10,000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# users.notifications_read_at&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_all_read!&lt;/span&gt;
  &lt;span class="n"&gt;update_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:notifications_read_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unread_count&lt;/span&gt;
  &lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;read_at: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"created_at &amp;gt; ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifications_read_at&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;limit(100)&lt;/code&gt; is deliberate. Nobody needs to know they have 8,412 unread notifications — "99+" is the same information at a fraction of the cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7c7fmmarukda3beb0g7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7c7fmmarukda3beb0g7w.png" alt=" " width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch in production
&lt;/h2&gt;

&lt;p&gt;Four numbers tell you whether this is healthy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out lag&lt;/strong&gt; — event created → last notification row written&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery lag&lt;/strong&gt; — notification created → provider accepted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue depth per channel&lt;/strong&gt; — the first thing to spike when a provider degrades&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery failure rate by channel&lt;/strong&gt; — a broken push cert looks exactly like a quiet week unless you're graphing this&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Emit them as counters and histograms and alert on the p95, not the average. Notification systems fail at the tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write &lt;strong&gt;one event&lt;/strong&gt;, enqueue &lt;strong&gt;one job&lt;/strong&gt; in the request. Never loop.&lt;/li&gt;
&lt;li&gt;Fan out in &lt;strong&gt;chunks&lt;/strong&gt;, insert in &lt;strong&gt;bulk&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Put a &lt;strong&gt;unique idempotency key&lt;/strong&gt; on every notification so retries are free.&lt;/li&gt;
&lt;li&gt;Split delivery into &lt;strong&gt;per-channel jobs on per-channel queues&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Solve the unread badge with a &lt;strong&gt;partial index and a watermark&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires a new gem or a new service. It's the same tables you'd have written anyway — just arranged so that the number of recipients stops being your app's problem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How are you handling fan-out in your app — dedicated tables, an outbox, or a third-party service? I'd be curious what's held up at scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Hidden Cost of Missing or Wrong Database Indexes in Rails</title>
      <dc:creator>A.Talaat</dc:creator>
      <pubDate>Wed, 17 Jun 2026 19:17:43 +0000</pubDate>
      <link>https://dev.to/a-talaat-aly/the-hidden-cost-of-missing-or-wrong-database-indexes-in-rails-ni2</link>
      <guid>https://dev.to/a-talaat-aly/the-hidden-cost-of-missing-or-wrong-database-indexes-in-rails-ni2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvz8d9hinz8oj8h8x8ao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvz8d9hinz8oj8h8x8ao.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you think your SQL Query Is Fine?! &lt;br&gt;
Lets today talk about the hidden cost of missing or wrong Database indexes in Rails.&lt;/p&gt;

&lt;p&gt;A few years ago, I was investigating a Rails endpoint that suddenly became slow.&lt;/p&gt;

&lt;p&gt;Nothing unusual had happened.&lt;br&gt;
No major deployment.&lt;br&gt;
No infrastructure changes.&lt;br&gt;
No traffic spike.&lt;br&gt;
The endpoint that normally responded in under 100ms was now taking several seconds.&lt;br&gt;
The Rails logs looked clean.&lt;br&gt;
The SQL query looked reasonable.&lt;br&gt;
The application servers were healthy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;The problem .........  &lt;br&gt;
A missing database index.&lt;br&gt;
One small database optimization turned a multi-second query into a millisecond operation.&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you've ever experienced unexplained performance degradation in a Rails application, there's a good chance missing indexes were involved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;let's go deeper in indexing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1- What Is a Database Index?&lt;/strong&gt;&lt;br&gt;
Think of a database index as the index section at the back of a book.&lt;br&gt;
Without an index, finding a specific topic means reading page after page until you locate the information.&lt;br&gt;
With an index, you can jump directly to the correct page.&lt;br&gt;
Databases work in a similar way.&lt;br&gt;
When a query filters records using indexed columns, the database can locate matching rows quickly.&lt;br&gt;
Without an index, the database may be forced to scan the entire table.&lt;br&gt;
And that's where trouble begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2- The Problem Isn't Today&lt;/strong&gt;&lt;br&gt;
One of the reasons missing indexes are so dangerous is that they often go unnoticed during development.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Consider this scenario:&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development database: users table contains 500 rows&lt;/li&gt;
&lt;li&gt;Production database: users table contains 50 million rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A query that feels instant during development may become painfully slow in production.&lt;br&gt;
The larger your dataset becomes, the more expensive table scans become.&lt;br&gt;
This is why indexing is ultimately a scalability concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3- Why Missing Indexes Cause Performance Problems&lt;/strong&gt;&lt;br&gt;
When a query searches for records, the database has two options:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Best Case (Use an index).&lt;/u&gt;&lt;br&gt;
Locate matching rows quickly&lt;br&gt;
Return results&lt;br&gt;
Done&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Worst Case (Perform a full table scan).&lt;/u&gt;&lt;br&gt;
Read row 1&lt;br&gt;
Read row 2&lt;br&gt;
Read row 3&lt;br&gt;
...&lt;br&gt;
Read row 50,000,000&lt;/p&gt;

&lt;p&gt;The second option consumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/astmdesign/why-your-ruby-on-rails-application-has-high-cpu-usage-3o58"&gt;More CPU&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;More memory&lt;/li&gt;
&lt;li&gt;More disk I/O&lt;/li&gt;
&lt;li&gt;More query execution time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests become slower&lt;/li&gt;
&lt;li&gt;Application workers remain busy longer&lt;/li&gt;
&lt;li&gt;CPU usage increases&lt;/li&gt;
&lt;li&gt;Database load grows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually users begin noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4- Common Places Where Missing Indexes Appear&lt;/strong&gt;&lt;br&gt;
Most Rails applications contain several predictable indexing candidates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Foreign Keys:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If your queries frequently search by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An index is usually required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status Columns:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frequently filtered columns are often strong indexing candidates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search Fields
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Email addresses, usernames, slugs, and external identifiers are common index targets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sorting Operations
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sorting large datasets can become expensive without proper indexing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5- Why Rails Developers Accidentally Create This Problem&lt;/strong&gt;&lt;br&gt;
Most missing index issues come from one of these situations.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Feature-Driven Development&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A feature is delivered.&lt;/li&gt;
&lt;li&gt;Tests pass.&lt;/li&gt;
&lt;li&gt;Users are happy.&lt;/li&gt;
&lt;li&gt;Nobody thinks about query execution plans.&lt;/li&gt;
&lt;li&gt;The feature works today.&lt;/li&gt;
&lt;li&gt;The performance problem appears six months later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;Small Development Databases&lt;/u&gt;&lt;br&gt;
Local databases rarely contain production-sized datasets.&lt;br&gt;
Queries that appear instant locally may behave very differently with millions of records.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Schema Evolution&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Applications grow.&lt;/li&gt;
&lt;li&gt;New reports are added.&lt;/li&gt;
&lt;li&gt;New filters are introduced.&lt;/li&gt;
&lt;li&gt;New business requirements emerge.&lt;/li&gt;
&lt;li&gt;The indexing strategy that worked last year may no longer be sufficient today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6- How to Avoid Missing Indexes&lt;/strong&gt;&lt;br&gt;
The easiest strategy is simple:&lt;br&gt;
Whenever you introduce a new query pattern, ask yourself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Will this column be frequently searched, filtered, joined, or sorted?&lt;br&gt;
If the answer is yes, consider whether an index should exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;customer_id: &lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These queries often benefit from indexing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7- Helpful Tools Every Rails Engineer Should Know&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EXPLAIN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first tool every engineer should learn.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails can show the database execution plan and reveal whether indexes are being used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PgHero&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most valuable tools for PostgreSQL-powered Rails applications.&lt;/p&gt;

&lt;p&gt;PgHero can identify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing indexes&lt;/li&gt;
&lt;li&gt;Unused indexes&lt;/li&gt;
&lt;li&gt;Slow queries&lt;/li&gt;
&lt;li&gt;Expensive database operations&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Many Rails teams discover performance issues with PgHero long before users report them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bullet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most engineers know Bullet for detecting N+1 Queries.&lt;br&gt;
However, it can also provide useful performance insights during development.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New Relic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provides visibility into:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Slow database transactions&lt;/li&gt;
&lt;li&gt;Query performance&lt;/li&gt;
&lt;li&gt;Endpoint bottlenecks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Excellent for identifying problematic database behavior in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Datadog APM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful for tracing application requests and understanding where time is spent between Rails and PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8- When Not to Add an Index&lt;/strong&gt;&lt;br&gt;
A common mistake is believing every column needs an index.&lt;br&gt;
Indexes are powerful, but they are not free.&lt;/p&gt;

&lt;p&gt;Every index:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consumes storage&lt;/li&gt;
&lt;li&gt;Slows INSERT operations&lt;/li&gt;
&lt;li&gt;Slows UPDATE operations&lt;/li&gt;
&lt;li&gt;Increases maintenance overhead&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to index everything.&lt;br&gt;
The goal is to index the right things.&lt;br&gt;
Good indexing is a balancing act.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>The Silent Performance Killer in Rails: Understanding and Fixing N+1 Queries</title>
      <dc:creator>A.Talaat</dc:creator>
      <pubDate>Tue, 16 Jun 2026 08:44:45 +0000</pubDate>
      <link>https://dev.to/a-talaat-aly/the-silent-performance-killer-in-rails-understanding-and-fixing-n1-queries-2ibb</link>
      <guid>https://dev.to/a-talaat-aly/the-silent-performance-killer-in-rails-understanding-and-fixing-n1-queries-2ibb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24aoyggjtav9r0y195f5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24aoyggjtav9r0y195f5.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
If you've ever investigated a slow Rails endpoint, chances are you've encountered one of the most common performance problems in the Rails ecosystem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N+1 Queries.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The scary part?&lt;/p&gt;

&lt;p&gt;Your application can appear perfectly healthy during development, pass all tests, and work flawlessly in production for months.&lt;/p&gt;

&lt;p&gt;Then one day your dataset grows, traffic increases, and suddenly an endpoint that used to respond in 100ms now takes several seconds.&lt;/p&gt;

&lt;p&gt;No infrastructure changes.&lt;/p&gt;

&lt;p&gt;No database outages.&lt;/p&gt;

&lt;p&gt;No major code deployments.&lt;/p&gt;

&lt;p&gt;Just a silent performance killer hiding inside your ActiveRecord relationships.&lt;/p&gt;

&lt;p&gt;In this article we'll explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What N+1 Queries are&lt;/li&gt;
&lt;li&gt;Why they happen so frequently in Rails applications&lt;/li&gt;
&lt;li&gt;How they impact performance&lt;/li&gt;
&lt;li&gt;Techniques to prevent them&lt;/li&gt;
&lt;li&gt;Tools that can help detect them before they reach production&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  What Is an N+1 Query?
&lt;/h1&gt;

&lt;p&gt;An N+1 Query occurs when your application executes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One query to fetch a collection of records&lt;/li&gt;
&lt;li&gt;One additional query for each record in that collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's imagine a simple blog application.&lt;/p&gt;

&lt;p&gt;Each Post belongs to an Author.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance this looks innocent.&lt;/p&gt;

&lt;p&gt;However, Rails may execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of executing two queries, the application executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 + 100 = 101 queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is known as an N+1 Query.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Does This Happen?
&lt;/h1&gt;

&lt;p&gt;The answer lies in one of Rails' most powerful features:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy Loading.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ActiveRecord delays loading associated records until they are actually needed.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not load the author when the post is fetched.&lt;/p&gt;

&lt;p&gt;It loads the author only when that line is executed.&lt;/p&gt;

&lt;p&gt;This behavior is incredibly convenient because it reduces unnecessary database access.&lt;/p&gt;

&lt;p&gt;However, when used inside loops, it can accidentally create hundreds or even thousands of additional queries.&lt;/p&gt;

&lt;p&gt;The framework is doing exactly what we asked it to do.&lt;/p&gt;

&lt;p&gt;The problem is that we didn't realize how many times we were asking.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why N+1 Queries Are Dangerous
&lt;/h1&gt;

&lt;p&gt;Many engineers focus on query execution time.&lt;/p&gt;

&lt;p&gt;The real problem is query volume.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 users → 11 queries&lt;/li&gt;
&lt;li&gt;100 users → 101 queries&lt;/li&gt;
&lt;li&gt;1,000 users → 1,001 queries&lt;/li&gt;
&lt;li&gt;10,000 users → 10,001 queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As your data grows, the number of database round trips grows with it.&lt;/p&gt;

&lt;p&gt;This creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increased database load&lt;/li&gt;
&lt;li&gt;Higher application CPU usage&lt;/li&gt;
&lt;li&gt;More network overhead&lt;/li&gt;
&lt;li&gt;Longer request times&lt;/li&gt;
&lt;li&gt;Reduced scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What worked perfectly in staging may become a production nightmare months later.&lt;/p&gt;




&lt;h1&gt;
  
  
  How to Avoid N+1 Queries
&lt;/h1&gt;

&lt;p&gt;The most common solution is eager loading.&lt;/p&gt;

&lt;p&gt;Rails allows you to preload associations before they are accessed.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails now loads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only two queries.&lt;/p&gt;

&lt;p&gt;No matter how many posts are returned.&lt;/p&gt;

&lt;p&gt;This is one of the easiest performance wins available in Rails.&lt;/p&gt;




&lt;h1&gt;
  
  
  Common Places Where N+1 Queries Hide
&lt;/h1&gt;

&lt;p&gt;Many engineers look only at controllers.&lt;/p&gt;

&lt;p&gt;In reality, N+1 issues often appear in:&lt;/p&gt;

&lt;h3&gt;
  
  
  API Serializers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;PostSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Jbuilder Templates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author_name&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GraphQL Resolvers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  View Templates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Background Jobs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plan&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any place that loops through records and accesses associations can potentially create N+1 Queries.&lt;/p&gt;




&lt;h1&gt;
  
  
  Helpful Tools for Detecting N+1 Queries
&lt;/h1&gt;

&lt;p&gt;One of the best investments a Rails team can make is detecting N+1 Queries early.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bullet Gem
&lt;/h3&gt;

&lt;p&gt;The most popular N+1 detection tool in the Rails ecosystem.&lt;/p&gt;

&lt;p&gt;It automatically alerts developers when associations should be eager loaded.&lt;/p&gt;

&lt;p&gt;Example notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USE eager loading detected
Add to your query: .includes(:author)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Rack Mini Profiler
&lt;/h3&gt;

&lt;p&gt;Provides detailed request analysis.&lt;/p&gt;

&lt;p&gt;Helps identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow queries&lt;/li&gt;
&lt;li&gt;Query counts&lt;/li&gt;
&lt;li&gt;Rendering bottlenecks&lt;/li&gt;
&lt;li&gt;Performance hotspots&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Skylight
&lt;/h3&gt;

&lt;p&gt;Excellent for production monitoring.&lt;/p&gt;

&lt;p&gt;It helps visualize database activity and identify endpoints suffering from excessive query counts.&lt;/p&gt;




&lt;h3&gt;
  
  
  New Relic
&lt;/h3&gt;

&lt;p&gt;Provides detailed transaction tracing and database monitoring.&lt;/p&gt;

&lt;p&gt;Very useful for identifying performance degradation caused by query-heavy endpoints.&lt;/p&gt;




&lt;h3&gt;
  
  
  Datadog APM
&lt;/h3&gt;

&lt;p&gt;Widely used in large-scale Rails environments.&lt;/p&gt;

&lt;p&gt;Offers visibility into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL execution&lt;/li&gt;
&lt;li&gt;Request traces&lt;/li&gt;
&lt;li&gt;Database latency&lt;/li&gt;
&lt;li&gt;Application bottlenecks&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;N+1 Queries are dangerous because they don't usually break functionality.&lt;/p&gt;

&lt;p&gt;Your tests pass.&lt;/p&gt;

&lt;p&gt;Your code looks clean.&lt;/p&gt;

&lt;p&gt;Users may not notice the problem immediately.&lt;/p&gt;

&lt;p&gt;But as data grows, the hidden cost grows with it.&lt;/p&gt;

&lt;p&gt;Understanding N+1 Queries is one of the first steps toward becoming a performance-conscious Rails engineer.&lt;/p&gt;

&lt;p&gt;And the good news?&lt;/p&gt;

&lt;p&gt;Many N+1 issues can be fixed with a single line of code.&lt;/p&gt;

&lt;p&gt;In the next article, we'll dive deeper into advanced eager loading strategies, compare &lt;code&gt;includes&lt;/code&gt;, &lt;code&gt;preload&lt;/code&gt;, and &lt;code&gt;eager_load&lt;/code&gt;, and discuss how to detect N+1 Queries in production environments before your users notice them.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>Why your Ruby on Rails application has high CPU usage?</title>
      <dc:creator>A.Talaat</dc:creator>
      <pubDate>Sun, 14 Jun 2026 10:45:13 +0000</pubDate>
      <link>https://dev.to/a-talaat-aly/why-your-ruby-on-rails-application-has-high-cpu-usage-3o58</link>
      <guid>https://dev.to/a-talaat-aly/why-your-ruby-on-rails-application-has-high-cpu-usage-3o58</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Have you ever received an alert saying:&lt;br&gt;
CPU usage is above 90%&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or maybe one of your production servers suddenly became slow, requests started timing out, and users began reporting performance issues.&lt;/p&gt;

&lt;p&gt;As Rails engineers, one of the most common production incidents we face is unexpectedly high CPU utilization.&lt;/p&gt;

&lt;p&gt;The tricky part?&lt;br&gt;
High CPU usage is rarely the actual problem.&lt;/p&gt;

&lt;p&gt;It's usually a symptom of something else happening inside your application, infrastructure, database, background jobs, or even your code architecture.&lt;/p&gt;

&lt;p&gt;I will explore the most common reasons behind high CPU consumption in Ruby on Rails applications. &lt;br&gt;
won't go too deep into each topic because every single one deserves.&lt;/p&gt;

&lt;p&gt;Let's get started with the reasons:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;&lt;a href="https://dev.to/astmdesign/the-silent-performance-killer-in-rails-understanding-and-fixing-n1-queries-2ibb"&gt;1. N+1 Database Queries&lt;/a&gt;&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow API responses&lt;/li&gt;
&lt;li&gt;Increased database load&lt;/li&gt;
&lt;li&gt;High application CPU usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;&lt;a href="http://dev.to/astmdesign/the-hidden-cost-of-missing-or-wrong-database-indexes-in-rails-ni2"&gt;2. Missing Database Indexes&lt;/a&gt;&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow endpoints&lt;/li&gt;
&lt;li&gt;High database CPU&lt;/li&gt;
&lt;li&gt;Increasing request latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;3. Inefficient ActiveRecord Queries&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Excessive memory consumption&lt;/li&gt;
&lt;li&gt;Long request execution times&lt;/li&gt;
&lt;li&gt;CPU spikes during large queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;4. Expensive Background Jobs&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU spikes during scheduled jobs&lt;/li&gt;
&lt;li&gt;Delayed Sidekiq queues&lt;/li&gt;
&lt;li&gt;Increased worker execution times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;5. Memory Pressure and Garbage Collection&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU spikes without increased traffic&lt;/li&gt;
&lt;li&gt;Slow request processing&lt;/li&gt;
&lt;li&gt;High GC activity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;6. Excessive JSON Serialization&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API endpoints slower than expected&lt;/li&gt;
&lt;li&gt;High CPU during response generation&lt;/li&gt;
&lt;li&gt;Large response payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;7. Traffic Spikes and Uneven Load Distribution&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only specific nodes experience high CPU&lt;/li&gt;
&lt;li&gt;Random server crashes&lt;/li&gt;
&lt;li&gt;Uneven infrastructure metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;8. External Service Bottlenecks&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Growing request queues&lt;/li&gt;
&lt;li&gt;Increased response times&lt;/li&gt;
&lt;li&gt;Thread pool saturation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;9. Inefficient Caching Strategies&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeated database queries&lt;/li&gt;
&lt;li&gt;CPU spikes during peak traffic&lt;/li&gt;
&lt;li&gt;Reduced cache hit ratio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;10. Ruby Code That Doesn't Scale&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU increases with data growth&lt;/li&gt;
&lt;li&gt;Slow batch operations&lt;/li&gt;
&lt;li&gt;Performance degradation over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;br&gt;
When CPU usage becomes high, avoid jumping directly to conclusions.&lt;br&gt;
The real challenge isn't reducing CPU usage.&lt;br&gt;
The real challenge is identifying what's consuming the CPU.&lt;br&gt;
In future articles, we'll dive deeper into each of these topics, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting N+1 queries in production&lt;/li&gt;
&lt;li&gt;Database indexing strategies for Rails applications&lt;/li&gt;
&lt;li&gt;Sidekiq performance tuning&lt;/li&gt;
&lt;li&gt;Ruby Garbage Collection optimization&lt;/li&gt;
&lt;li&gt;API serialization best practices&lt;/li&gt;
&lt;li&gt;Infrastructure troubleshooting techniques&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember:&lt;br&gt;
High CPU is usually a symptom. Your job as an engineer is to find the disease.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
