<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="https://shey.ca/feed.xml" rel="self" type="application/atom+xml" /><link href="https://shey.ca/" rel="alternate" type="text/html" /><updated>2025-12-10T20:51:57-05:00</updated><id>https://shey.ca/feed.xml</id><subtitle>Fractional Postgresql DBA, Ruby on Rails Developer, and Data Engineer.</subtitle><entry><title type="html">Five PostgreSQL Anti-Patterns</title><link href="https://shey.ca/2025/09/12/five-db-anti-patterns.html" rel="alternate" type="text/html" title="Five PostgreSQL Anti-Patterns" /><published>2025-09-12T13:25:00-04:00</published><updated>2025-09-12T13:25:00-04:00</updated><id>https://shey.ca/2025/09/12/five-db-anti-patterns</id><content type="html" xml:base="https://shey.ca/2025/09/12/five-db-anti-patterns.html"><![CDATA[<p>As Rails developers, we’re taught that code is the expensive part and that the database will mostly take care of itself, as long as you throw enough RAM, CPU, and indexes at it. But that approach doesn’t lead to high performance.</p>

<p>High-performance, low-latency apps are table stakes now. Users expect things to be fast, and budgets no longer support just throwing money at the problem. In modern high-throughput, resource-constrained environments, every byte and every millisecond matters. And the database is often the biggest leverage point.</p>

<p>Rails and SaaS apps often stumble in the same predictable ways: UUIDv4 keys, wide tables, and bloated indexes keep showing up, adding latency and cost as data grows.</p>

<p>In this post, I’ll cover five common Postgres anti-patterns I see in production Rails setups, and what to do instead.</p>

<hr />

<h2 id="1-using-uuidv4-as-primary-keys">1. Using <code class="language-plaintext highlighter-rouge">UUIDv4</code> as Primary Keys</h2>

<h3 id="why-its-a-problem">Why it’s a problem</h3>

<p>Because UUIDv4 is completely random, every insert lands in a different place in the index. That randomness forces Postgres to spread keys across way more pages than necessary. More pages means more I/O and more shuffling to find records that match a query. Even if you throw all the RAM in the world at it, UUIDv4 still forces the database to work harder, and the performance hit is real, even if it’s not always obvious or easy to trace.</p>

<h3 id="what-to-do-instead">What to do instead</h3>

<ul>
  <li>Use <code class="language-plaintext highlighter-rouge">BIGINT</code>s. They support up to 9 quintillion values, more than enough for anything a Rails app will ever need. They’re also compact (just 8 bytes) and naturally sequential, so inserts tend to land on the same page. That means fewer page splits, faster inserts, and better performance for lookups and range scans.</li>
  <li>If you really need globally unique IDs, use <code class="language-plaintext highlighter-rouge">UUIDv7</code>. They’re time-ordered, so new records tend to land on the same page or the next sequential page. They use 16 bytes (twice the size of a <code class="language-plaintext highlighter-rouge">BIGINT</code>), so you’re paying more in storage, but you still avoid the page explosion problem of UUIDv4.</li>
</ul>

<hr />

<h2 id="2-ultra-wide-tables">2. Ultra-Wide Tables</h2>

<h3 id="why-its-a-problem-1">Why it’s a problem</h3>

<p>Tables with lots of columns hurt performance in a few ways.</p>

<p>First, there’s the obvious cost of pulling all that data. It still has to come off disk, travel over the network, and get instantiated into large in-memory objects. Second, even when you’re not querying everything, Postgres still has to manage all those extra columns. TOAST (Postgres’s system for handling large field values) adds more I/O, more CPU work, and more complexity. And finally, the queries themselves get huge and unreadable. Most slow query logs won’t even capture the full text, which means the part of the SQL that could actually help with indexing or optimization is lost.</p>

<h3 id="what-to-do-instead-1">What to do instead</h3>

<ul>
  <li>The best fix is to split the table. Keep the core attributes in one table and move the sparse or optional fields into another, connected with a <code class="language-plaintext highlighter-rouge">has_one</code> relationship. That way most queries only touch the smaller table, and you only hit the larger one when the app actually needs those extra fields.</li>
  <li>If you’re stuck with a wide table, be deliberate about which data is selected. Use methods like <code class="language-plaintext highlighter-rouge">pluck</code> or pass an explicit column list to <code class="language-plaintext highlighter-rouge">select</code> to avoid pulling back more data than needed. This won’t fix the schema, but it will reduce payload size and help maintain faster response times.</li>
</ul>

<hr />

<h2 id="3-letting-indexes-get-big-and-slow">3. Letting Indexes Get Big and Slow</h2>

<h3 id="why-its-a-problem-2">Why it’s a problem</h3>

<p>In PostgreSQL, every update or delete leaves behind dead index entries that don’t get cleaned up right away.</p>

<p>On high-churn tables like <code class="language-plaintext highlighter-rouge">delayed_jobs</code>, <code class="language-plaintext highlighter-rouge">solid_queue_jobs</code>, and <code class="language-plaintext highlighter-rouge">good_jobs</code>, indexes grow fast and lose efficiency quickly. A query that took 2 ms with a fresh index can be crawling at 20 ms only a few hours later. As those dead entries accumulate, the index gets spread across more pages than necessary. That means more pages to scan, more random I/O, higher memory usage, and more CPU time spent navigating bloated index trees.</p>

<h3 id="what-to-do-instead-2">What to do instead</h3>

<ul>
  <li>Run <code class="language-plaintext highlighter-rouge">REINDEX CONCURRENTLY</code> regularly on high-churn tables to rebuild indexes. Reindexing clears out dead entries and helps restore index efficiency and performance.</li>
  <li>For very large or heavily read indexes, consider using <code class="language-plaintext highlighter-rouge">pg_repack</code>. Like <code class="language-plaintext highlighter-rouge">REINDEX CONCURRENTLY</code>, it rebuilds indexes without blocking reads and writes, but it does so in parallel, which results in a smaller online performance hit. The trade-off is that it requires temporary disk space roughly equal to the size of the index.</li>
  <li>Track index health regularly. The <code class="language-plaintext highlighter-rouge">pgstattuple</code> extension provides detailed metrics on density, making it easier to identify when an index has become inefficient and needs reindexing.</li>
</ul>

<hr />

<h2 id="4-storing-statuses-as-strings">4. Storing Statuses as Strings</h2>

<h3 id="why-its-a-problem-3">Why it’s a problem</h3>

<p>Storing statuses as plain text like <code class="language-plaintext highlighter-rouge">'pending'</code>, <code class="language-plaintext highlighter-rouge">'shipped'</code>, or <code class="language-plaintext highlighter-rouge">'canceled'</code> feels easy and flexible. You can add new states whenever without too much work. I made the same mistake, encoding <code class="language-plaintext highlighter-rouge">'up'</code> and <code class="language-plaintext highlighter-rouge">'down'</code> states as strings in <a href="https://httpscout.io/">httpscout</a>.</p>

<p>The problem with storing statuses as text is that the data isn’t dense. Strings use more bytes than necessary to represent a small, fixed set of states. That extra size makes rows bigger, indexes less efficient, and forces Postgres to push more data through disk and memory than it needs to. Multiply that overhead across millions of rows, and it adds up to a real performance cost.</p>

<h3 id="what-to-do-instead-3">What to do instead</h3>

<ul>
  <li>Use a small integer column to encode statuses. It’s a denser representation that requires less storage and keeps rows and indexes smaller. In Rails, you can use an enum to keep meaningful names:</li>
</ul>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Order</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">enum</span> <span class="ss">status: </span><span class="p">{</span> <span class="ss">pending: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">shipped: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">canceled: </span><span class="mi">2</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This gives you human-readable statuses in the app code, while the database stores them in a more compact and efficient form.</p>

<hr />

<h2 id="5-leaving-around-dead-and-unused-indexes">5. Leaving Around Dead and Unused Indexes</h2>

<h3 id="why-its-a-problem-4">Why it’s a problem</h3>

<p>In Rails, it’s easy to add indexes. We’re taught to be proactive and treat them as free. But every index has a cost.</p>

<p>Whenever a record is inserted, updated, or deleted, Postgres has to update every relevant index, even the ones no queries are using. As the number of indexes grows, write performance gets worse. I’ve seen p90 insert times jump from sub-millisecond to over 15 ms, purely because of how many indexes the table had. That’s time your web thread is blocked, stuck waiting on Postgres instead of handling the next request.</p>

<h3 id="what-to-do-instead-4">What to do instead</h3>

<ul>
  <li>Track index usage over time. If Postgres hasn’t used an index for a reasonable period, drop it.</li>
  <li>In Rails, you can use <a href="https://github.com/pawurb/rails-pg-extras?tab=readme-ov-file#unused_indexes">RailsPgExtras</a> to surface unused indexes. If an index has zero scans and isn’t unique, it’s probably safe to drop.</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>PostgreSQL can be incredibly fast, but only if you use it with care. These anti-patterns show up quietly: one UUID primary key, one index, one column at a time. Before long, the app slows down, queries drag, infra bills grow, and the app spends more and more time waiting on the database.</p>

<p>Performance isn’t magic. It’s the result of small, intentional decisions that add up over time: lean tables, healthy indexes, and regular maintenance. In high-throughput systems, those details make all the difference.</p>

<hr />

<h2 id="notes-links-and-references">Notes, Links, and References</h2>

<ol>
  <li><a href="https://dev.to/umangsinha12/postgresql-uuid-performance-benchmarking-random-v4-and-time-based-v7-uuids-n9b">PostgreSQL UUID Performance: Benchmarking Random (v4) and Time-based (v7) UUIDs</a></li>
  <li><a href="https://uuid7.com/">UUIDv7: The Time-Sortable Identifier for Modern Databases</a></li>
  <li><a href="https://edu.postgrespro.com/2dintro/08_admin_maintenance.html">Estimating Bloat of Tables and Indexes</a></li>
  <li><a href="https://wiki.postgresql.org/wiki/Index_Maintenance">PostgreSQL Wiki - Index Maintenance</a></li>
  <li><a href="https://andyatkinson.com/blog/2021/09/28/pg-repack">Using pg_repack to Rebuild Indexes</a></li>
  <li><a href="https://pganalyze.com/blog/5mins-postgres-TOAST-performance">Performance Implications of TOAST</a></li>
  <li><a href="https://github.com/pawurb/rails-pg-extras">Rails PG Extras</a></li>
</ol>]]></content><author><name></name></author><category term="postgresql" /><summary type="html"><![CDATA[As Rails developers, we’re taught that code is the expensive part and that the database will mostly take care of itself, as long as you throw enough RAM, CPU, and indexes at it. But that approach doesn’t lead to high performance.]]></summary></entry><entry><title type="html">UUIDv4: My Biggest Database Performance Mistake</title><link href="https://shey.ca/2025/08/10/my-biggest-db-mistake.html" rel="alternate" type="text/html" title="UUIDv4: My Biggest Database Performance Mistake" /><published>2025-08-10T13:25:00-04:00</published><updated>2025-08-10T13:25:00-04:00</updated><id>https://shey.ca/2025/08/10/my-biggest-db-mistake</id><content type="html" xml:base="https://shey.ca/2025/08/10/my-biggest-db-mistake.html"><![CDATA[<p>A long time ago, I made a call I thought was smart and scalable: using UUIDv4 as the primary key for alerting data. It seemed forward-looking.</p>

<p>I would avoid potential problems with sharing the database later, didn’t have to worry about auto-increment overflow, eliminated the risk of leaking sequential IDs that could allow record enumeration attacks, and took a tiny bit of pressure off the database by generating IDs in the app instead of the DB. I thought I was clever.</p>

<p>That feeling lasted until we onboarded our first major client. Suddenly, queries against UUIDv4-keyed tables were <em>slow</em> “<code class="language-plaintext highlighter-rouge">work_mem</code> set to 1KB” slow. No matter how carefully we indexed, we couldn’t get the same performance we were used to with traditional keys.</p>

<p>The core problem is that UUIDv4 values are completely random, so inserts land all over the key space. This blows up index locality, forcing the B-tree to constantly split and rebalance. Pages end up half-full and scattered, which means fewer of them stay resident in memory.</p>

<p>UUIDs are also 16 bytes, double the width of a <code class="language-plaintext highlighter-rouge">BIGINT</code> and quadruple an <code class="language-plaintext highlighter-rouge">INTEGER</code>, so every index entry and table row pointer has more overhead. That extra width also means fewer entries per page, more pages overall, and more cache churn. Once those pages fall out of shared buffers, Postgresql has to hit disk to read them back, and query latency spikes.</p>

<p>Something else I learned, important enough to call out separately, is that signed <code class="language-plaintext highlighter-rouge">BIGINT</code>s support values from <code class="language-plaintext highlighter-rouge">-9,223,372,036,854,775,808</code> to <code class="language-plaintext highlighter-rouge">9,223,372,036,854,775,807</code>. That’s over <strong>9 quintillion</strong> IDs. Auto-increment exhaustion is simply not a realistic concern for most applications. It’s not a good reason to choose UUIDv4.</p>

<p>In the end, we re-keyed everything to use <code class="language-plaintext highlighter-rouge">BIGINT</code>. Performance returned: smaller indexes, faster writes, and query times that made sense again. The fix worked, but it came at the cost of weeks of worry and re-work. The takeaway was clear: <strong>if you can avoid UUIDs, do it.</strong></p>

<h2 id="what-about-uuidv7">What About UUIDv7?</h2>

<p>UUIDv7 indexes better than UUIDv4, but back when I made this mistake, <a href="https://uuid7.com/">UUIDv7</a> didn’t exist. If it had, I might have made a different choice.</p>

<p>V7 embeds a millisecond-precision timestamp into the UUID, which means inserts are monotoniclly increasing instead of fully random. You still get globally unique identifiers but also have less spaced out indexes. This results in better insert performance, smaller indexes, and faster selects. These are all the properties you want for a high-performance index.</p>

<p><a href="https://dev.to/umangsinha12/postgresql-uuid-performance-benchmarking-random-v4-and-time-based-v7-uuids-n9b">Benchmarks</a> back this up, with tests showing UUIDv7 insert performance up to <strong>30–35% faster</strong> than UUIDv4, producing indexes around <strong>22% smaller</strong>, and yield a <strong>54% reduction</strong> in query execution time on Postgresql.</p>

<h2 id="closing-advice">Closing Advice</h2>
<p>Avoid UUIDv4 for primary keys unless you truly have no alternative. The performance hit is real. If you can use BIGINT, do it. It’s faster, more compact, and you’re not running out of IDs in your lifetime. Also, there are some good human-readable ID libraries out there too if you want something nicer looking IDs. If you need IDs generated outside the database, across shards, or in a distributed system, then UUIDv7 or <a href="https://shopify.engineering/building-resilient-payment-systems">ULIDs</a>.</p>]]></content><author><name></name></author><category term="reliability" /><summary type="html"><![CDATA[A long time ago, I made a call I thought was smart and scalable: using UUIDv4 as the primary key for alerting data. It seemed forward-looking.]]></summary></entry><entry><title type="html">Controlled Failure / Practice Before It Breaks</title><link href="https://shey.ca/2025/06/27/practice-before-it-breaks.html" rel="alternate" type="text/html" title="Controlled Failure / Practice Before It Breaks" /><published>2025-06-27T23:25:00-04:00</published><updated>2025-06-27T23:25:00-04:00</updated><id>https://shey.ca/2025/06/27/practice-before-it-breaks</id><content type="html" xml:base="https://shey.ca/2025/06/27/practice-before-it-breaks.html"><![CDATA[<blockquote>
  <p>Some recent conversations touched on building a culture of reliability. This post explores one piece of that: practice.</p>
</blockquote>

<h2 id="practice-before-it-breaks">Practice Before It Breaks</h2>

<p>Being on call is tough. It’s stressful. The page comes in, adrenaline spikes, and you’re scrambling. It’s not a fun time.</p>

<p>One way to make that stress manageable is to practice. If you’ve never walked through a restart, or an alert, or a login issue before it breaks then of course you’re going to freeze when it does.</p>

<p>Drills give you a chance to interact with the system and figure things out while it’s still calm. You simulate fire ahead of time, so when the real thing happens, you don’t panic. You’ve seen what happens and you have an idea of what to expect.</p>

<h2 id="practice-in-pieces">Practice in Pieces</h2>

<p>Not every drill needs to simulate a catastrophe. Some just walk through a single action—loading a dashboard, restarting a service, deploying a config. Others mimic real-world pressure: a customer hammering on a dashboard, a long-running cron job, or support backfilling data.</p>

<p>That kind of mess is what actually causes most incidents. Not the infrastructure itself, but the messy edge cases—customers doing unexpected things, real load, application-layer bugs.</p>

<p>And while you can walk through the steps it’s better to actually perform them and see what really breaks.</p>

<h2 id="practice-in-prod">Practice in Prod</h2>

<p>Staging environments are incomplete representations of production. There’s rarely enough data, and logging and alerting usually aren’t fully wired up. But if that’s where you feel comfortable starting, that’s fine. Start there.</p>

<p>Eventually, though, you’ll want to practice in prod. Nothing beats production. It’s where you learn how an incident really unfolds.</p>

<h2 id="plan-your-practice">Plan Your Practice</h2>

<p>A drill isn’t supposed to be a surprise. No chaos monkey please, we don’t need more stress. So schedule the drill, pick a scope, and let other teams know what you’re doing.</p>

<h2 id="other-benefits">Other Benefits</h2>

<p>There’s more to practicing than just not panicking during an incident. Drills are also a way to bring people into the team, to share language and techniques, and to build camaraderie.</p>

<p>They’re also one of the simplest ways to share institutional knowledge (because no matter how much you document, some of it just lives in people’s heads). And sharing that builds trust – it helps people feel like they belong.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Practice doesn’t solve everything. But it lowers panic, surfaces the unknown, and builds trust.</p>

<hr />

<p>I’ve written elsewhere about <a href="https://shey.ca/2024/04/28/reduce-outages-with-weekly-operations-review-meeting.html">operational reviews</a>, and there’s more to say about observability, operability, and performance. This post focuses on practice, specifically, the benefits of practice.</p>]]></content><author><name></name></author><category term="reliability" /><summary type="html"><![CDATA[Some recent conversations touched on building a culture of reliability. This post explores one piece of that: practice.]]></summary></entry><entry><title type="html">Performance First Rails – Lessons From Production</title><link href="https://shey.ca/2025/06/07/performance-first-rails-lessons-from-production.html" rel="alternate" type="text/html" title="Performance First Rails – Lessons From Production" /><published>2025-06-07T11:00:00-04:00</published><updated>2025-06-07T11:00:00-04:00</updated><id>https://shey.ca/2025/06/07/performance-first-rails-lessons-from-production</id><content type="html" xml:base="https://shey.ca/2025/06/07/performance-first-rails-lessons-from-production.html"><![CDATA[<h1 id="performance-first-rails--lessons-from-production">Performance First Rails – Lessons From Production</h1>

<p><em>This post is a work in progress. I’ll keep expanding and refining it over time.</em></p>

<p>Most Rails performance problems aren’t about Ruby. They’re about data: how it’s accessed, serialized, cached, and moved through your system.</p>

<p>These are practices I’ve picked up from running Rails in production. Some are obvious, others less so.</p>

<hr />

<h2 id="️-data-first-always">🗃️ Data First, Always</h2>

<p>Your database is the bottleneck long before Ruby is.</p>

<ul>
  <li>Avoid N+1 queries. Yes, still.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">exists?</code> instead of <code class="language-plaintext highlighter-rouge">count &gt; 0</code>.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">pluck</code> to avoid loading columns you don’t need.</li>
  <li>Never call <code class="language-plaintext highlighter-rouge">.all</code>. Use pagination library or chunking with <code class="language-plaintext highlighter-rouge">.in_batches</code> instead.</li>
  <li>For massive tables, consider partitioning especially for time-based data.</li>
</ul>

<h3 id="indexing">Indexing</h3>

<ul>
  <li><a href="https://shey.ca/2024/04/09/high-performance-indexing-in-postgresql.html">Composite indexes</a> should reflect your actual query filters and sort order.</li>
  <li>Use <a href="https://www.postgresql.org/docs/current/indexes-partial.html">partial indexes</a> to address query skew and workload imbalances. If a single tenant generates the majority of reads on a table, create a targeted index for just their slice of data. This avoids full-table indexes and keeps read performance fast for high-traffic accounts.</li>
  <li>Too many indexes will slow down inserts and updates—index only what you read often.</li>
  <li>Monitor slow queries and experiment with different indexes.</li>
</ul>

<hr />

<h2 id="-redis">🟥 Redis</h2>

<p>Redis gets abused and forgotten.</p>

<p>For caching Redis, reduce how often data is written to disk. Optimize <code class="language-plaintext highlighter-rouge">save</code> and <code class="language-plaintext highlighter-rouge">appendonly</code> settings.
If you overload a single Redis instance, you’ll experience weird latency spikes.</p>

<ul>
  <li><strong>Split workloads</strong>:
    <ul>
      <li>One Redis for jobs (Sidekiq)</li>
      <li>One for caching (Rails.cache)</li>
      <li>One for ephemeral features (rate limiting, feature flags, etc.)</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="-sidekiq-queues--concurrency">🦵 Sidekiq Queues &amp; Concurrency</h2>

<ul>
  <li><a href="https://github.com/mperham/sidekiq/wiki/Advanced-Options#queues">More queues</a> = more polling, more complexity, more starvation risk.</li>
  <li>Sidekiq latency problems are often queue starvation or Redis slowness, not job logic.</li>
  <li>Tune concurrency to match job intensity. CPU-bound work needs fewer threads.</li>
</ul>

<hr />

<h2 id="-cache-aggressively">💾 Cache Aggressively</h2>

<p>The less work your database has to do, the faster everything else gets.</p>

<ul>
  <li>Use <a href="https://blog.appsignal.com/2024/08/14/an-introduction-to-http-caching-in-ruby-on-rails.html">HTTP caching</a> where applicable.</li>
  <li>Cache <a href="https://justin.searls.co/posts/html-fragment-caching-really-works-/">HTML fragments</a> for common UI blocks.</li>
  <li>Cache expensive queries and API responses.</li>
  <li>Use <a href="https://github.com/jsonapi-serializer/jsonapi-serializer"><code class="language-plaintext highlighter-rouge">jsonapi-serializer</code></a> for fast, structured JSON. It’s composable and cache-aware.</li>
  <li>Make sure you have TTL entries on your cache items.</li>
</ul>

<hr />

<h2 id="-pool-sizes--throughput">🌊 Pool Sizes &amp; Throughput</h2>

<p>Puma, Sidekiq, Postgres, Redis—they all have pool settings. If they’re misaligned, you’ll see weird timeouts and throughput drops.</p>

<ul>
  <li>Configure Postgresql and Redis’ connection pool size to <code class="language-plaintext highlighter-rouge">RAILS_MAX_THREADS</code> + 1.</li>
  <li>Higher thread and worker counts can overwhelm your database. Experiment.</li>
  <li>Monitor wait times in your connection pools. They’re early indicators of saturation.</li>
</ul>

<hr />

<h2 id="-ruby--gems">🚀 Ruby &amp; Gems</h2>

<p>Ruby performance matters, but it’s not the first lever.</p>

<ul>
  <li>Ruby 3.5 is <a href="https://railsatscale.com/2025-05-21-fast-allocations-in-ruby-3-5/">faster</a>. Use it.</li>
  <li>Enable <a href="https://speed.yjit.org/">YJIT</a>. It’s real now.</li>
  <li>Use <a href="https://engineering.appfolio.com/appfolio-engineering/2018/2/1/benchmarking-rubys-heap-malloc-tcmalloc-jemalloc"><code class="language-plaintext highlighter-rouge">jemalloc</code></a>.</li>
  <li>Configure <a href="https://github.com/shey/til/blob/main/rails/verify-jemalloc-ruby.md">MALLOC_CONF</a> to reduce memory fragmentation with jemalloc.</li>
</ul>

<p>Before tuning code, audit your gems:</p>

<ul>
  <li>Avoid gems that wrap everything in callbacks or monkey patches.</li>
  <li>Avoid gems with inefficient data access patterns—they don’t scale.</li>
</ul>

<hr />

<h2 id="️-still-to-come">🛰️ Still to Come</h2>

<ul>
  <li>Memory pressure and tuning for large multi-tenant workloads.</li>
  <li>Strategies for load testing with <a href="https://github.com/tsenart/vegeta"><code class="language-plaintext highlighter-rouge">vegeta</code></a>.</li>
  <li>Notes on replicas, analytics traffic, and write isolation.</li>
  <li>Horizontal scaling basics: how and why to split workloads across differently configured hosts.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Performance First Rails – Lessons From Production]]></summary></entry><entry><title type="html">Improving Asset Delivery Times with gzip_static</title><link href="https://shey.ca/2025/05/24/serving-assets-faster-nginx-gzip.html" rel="alternate" type="text/html" title="Improving Asset Delivery Times with gzip_static" /><published>2025-05-24T01:00:00-04:00</published><updated>2025-05-24T01:00:00-04:00</updated><id>https://shey.ca/2025/05/24/serving-assets-faster-nginx-gzip</id><content type="html" xml:base="https://shey.ca/2025/05/24/serving-assets-faster-nginx-gzip.html"><![CDATA[<p>So, <a href="https://httpscout.io/">HTTPScout’s</a> JavaScript bundle is 1.9MB.</p>

<p>Something’s clearly broken in my setup. It’s something I’ll fix one day. Right now I don’t have the brain juice. That said, I still want performance. A 1.9MB bundle is oppressive (and embarrassing), even with HTTP caching.</p>

<p>Anyhoo, I was reading ruby.social, and <a href="https://ruby.social/deck/@pushcx/114559678950415656">pushcx</a> published a bug list for <a href="https://lobste.rs/">lobste.rs</a>.</p>

<p>And! as luck would have it, one of the issues referenced using <a href="https://github.com/lobsters/lobsters/issues/1427">gzip_static</a> in nginx.</p>

<p>I immediately understood what I saw. I know I didn’t have that set, and it would make serving the giant js file faster.</p>

<p>It’s a simple change to the nginx config:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">location</span> <span class="p">~</span> <span class="sr">^/(assets)/</span> <span class="p">{</span>
  <span class="kn">gzip_static</span> <span class="no">on</span><span class="p">;</span>
  <span class="kn">root</span> <span class="n">/home/rails/lrt/current/public</span><span class="p">;</span>
  <span class="kn">expires</span> <span class="s">max</span><span class="p">;</span>
  <span class="kn">add_header</span> <span class="s">Cache-Control</span> <span class="s">private</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="before-and-after">Before And After</h2>

<p>Before enabling gzip_static, the JavaScript bundle was 1.9MB, the CSS was 548KB, and total transferred size came out to 2.6MB. Finish Time was 990 miliseconds.</p>

<p><a href="/assets/gzip/before-gzip.png"><img src="/assets/gzip/before-gzip.png" alt="Before" /></a></p>

<p>After the change, the JS dropped to 672KB, CSS to 72.4KB, and total transfer came down to 816KB. Finish time fell to 440. That’s 550 miliseconds faster!</p>

<p><a href="/assets/gzip/after-gzip.png"><img src="/assets/gzip/after-gzip.png" alt="After" /></a></p>

<h2 id="conclusion">Conclusion</h2>

<p>Enabling gzip_static took 550 ms off Finish Time and reduced the total payload by 1.7MB. That’s a single-line nginx config change with a huge payoff. The asset pipeline still needs work, but this gave the site the performance boost it needed.</p>

<h2 id="ps">P.S.</h2>

<p>I’m grateful that sites like <a href="https://ruby.social">ruby.social</a> and <a href="https://lobste.rs/">lobste.rs</a> exist.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[So, HTTPScout’s JavaScript bundle is 1.9MB.]]></summary></entry><entry><title type="html">Cleaner Mailer Views in Rails with the Presenter Pattern</title><link href="https://shey.ca/2025/04/19/presenter-pattern-in-ruby.html" rel="alternate" type="text/html" title="Cleaner Mailer Views in Rails with the Presenter Pattern" /><published>2025-04-19T01:00:00-04:00</published><updated>2025-04-19T01:00:00-04:00</updated><id>https://shey.ca/2025/04/19/presenter-pattern-in-ruby</id><content type="html" xml:base="https://shey.ca/2025/04/19/presenter-pattern-in-ruby.html"><![CDATA[<p>I love the presenter pattern. I use it in all my mailer views.</p>

<p>It’s deceptively simple, but incredibly useful. The idea is: move formatting and conditionals out of the view and into a plain Ruby object. That keeps logic in one place and makes the view easier to read.</p>

<p>I once shared it with a colleague, and they were genuinely blown away. Hopefully, you’ll find it just as useful.</p>

<h3 id="an-unrefactored-mailer-view">An unrefactored mailer view</h3>

<p>This view has some issues that make it a good candidate for refactoring:</p>

<ul>
  <li>It uses inline date formatting (<code class="language-plaintext highlighter-rouge">strftime</code>) right in the template.</li>
  <li>It calculates <code class="language-plaintext highlighter-rouge">days_left</code> directly inside the view logic, tying it to <code class="language-plaintext highlighter-rouge">Date.today</code> at render time.</li>
  <li>It contains conditional branching that controls not just structure, but wording.</li>
  <li>It handles pluralization manually: <code class="language-plaintext highlighter-rouge">"day#{'s' unless days_left == 1}"</code>.</li>
</ul>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hi <span class="cp">&lt;%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%&gt;</span>,

Just a reminder that your book "<span class="cp">&lt;%=</span> <span class="vi">@book</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%&gt;</span>" is due on
<span class="cp">&lt;%=</span> <span class="vi">@book</span><span class="p">.</span><span class="nf">due_date</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s2">"%B %d, %Y"</span><span class="p">)</span> <span class="cp">%&gt;</span>.

<span class="cp">&lt;%</span> <span class="n">days_left</span> <span class="o">=</span> <span class="p">(</span><span class="vi">@book</span><span class="p">.</span><span class="nf">due_date</span> <span class="o">-</span> <span class="no">Date</span><span class="p">.</span><span class="nf">today</span><span class="p">).</span><span class="nf">to_i</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">days_left</span> <span class="o">&lt;=</span> <span class="mi">2</span> <span class="cp">%&gt;</span>
  Please return it as soon as possible.

  It's due in just <span class="cp">&lt;%=</span> <span class="n">days_left</span> <span class="cp">%&gt;</span> day<span class="cp">&lt;%=</span> <span class="s1">'s'</span> <span class="k">if</span> <span class="n">days_left</span> <span class="o">!=</span> <span class="mi">1</span> <span class="cp">%&gt;</span>!
<span class="cp">&lt;%</span> <span class="k">else</span> <span class="cp">%&gt;</span>
  You still have <span class="cp">&lt;%=</span> <span class="n">days_left</span> <span class="cp">%&gt;</span> day<span class="cp">&lt;%=</span> <span class="s1">'s'</span> <span class="k">if</span> <span class="n">days_left</span> <span class="o">!=</span> <span class="mi">1</span> <span class="cp">%&gt;</span> left.
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

Thanks,

Your Friendly Library
</code></pre></div></div>

<h3 id="same-logic-cleaner-view">Same logic, cleaner view</h3>

<p>The refactor moved the conditionals, formatting, and pluralization out of the view and into a new presenter class. The result is a clean template with no conditional logic or formatting.</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hi <span class="cp">&lt;%=</span> <span class="vi">@presenter</span><span class="p">.</span><span class="nf">user_name</span> <span class="cp">%&gt;</span>,

Just a reminder that your book "<span class="cp">&lt;%=</span> <span class="vi">@presenter</span><span class="p">.</span><span class="nf">book_title</span> <span class="cp">%&gt;</span>" is due on <span class="cp">&lt;%=</span> <span class="vi">@presenter</span><span class="p">.</span><span class="nf">due_date</span> <span class="cp">%&gt;</span>.

<span class="cp">&lt;%=</span> <span class="vi">@presenter</span><span class="p">.</span><span class="nf">return_message</span> <span class="cp">%&gt;</span>

Thanks,

Your Friendly Library
</code></pre></div></div>

<h3 id="how-to-get-there">How to get there</h3>

<p>The new presenter class.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BookDueSoonPresenter</span>
  <span class="nb">attr_reader</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">:book</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">:,</span> <span class="n">book</span><span class="p">:)</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="n">user</span>
    <span class="vi">@book</span> <span class="o">=</span> <span class="n">book</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">user_name</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">name</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">subject</span>
    <span class="s2">"Reminder: Book Due Soon"</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">book_title</span>
    <span class="n">book</span><span class="p">.</span><span class="nf">title</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">due_date</span>
    <span class="n">book</span><span class="p">.</span><span class="nf">due_date</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s2">"%B %d, %Y"</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">days_left</span>
    <span class="p">(</span><span class="n">book</span><span class="p">.</span><span class="nf">due_date</span><span class="p">.</span><span class="nf">to_date</span> <span class="o">-</span> <span class="no">Date</span><span class="p">.</span><span class="nf">today</span><span class="p">).</span><span class="nf">to_i</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">return_message</span>
    <span class="n">messages</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">message_key</span><span class="p">)</span> <span class="o">%</span> <span class="p">{</span> <span class="ss">days: </span><span class="n">pluralized_days_left</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">message_key</span>
    <span class="n">days_left</span> <span class="o">&lt;=</span> <span class="mi">2</span> <span class="p">?</span> <span class="ss">:urgent</span> <span class="p">:</span> <span class="ss">:friendly</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">messages</span>
    <span class="p">{</span>
      <span class="ss">urgent:   </span><span class="s2">"Please return it as soon as possible — it's due in just %{days}!"</span><span class="p">,</span>
      <span class="ss">friendly: </span><span class="s2">"You still have %{days} left."</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">pluralized_days_left</span>
    <span class="s2">"</span><span class="si">#{</span><span class="n">days_left</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="s1">'day'</span><span class="p">.</span><span class="nf">pluralize</span><span class="p">(</span><span class="n">days_left</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Instantiating the presenter in the mailer.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">UserMailer</span> <span class="o">&lt;</span> <span class="no">ApplicationMailer</span>
  <span class="k">def</span> <span class="nf">book_due_soon</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">book</span><span class="p">)</span>
    <span class="vi">@presenter</span> <span class="o">=</span> <span class="no">BookDueSoonPresenter</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">book: </span><span class="n">book</span><span class="p">)</span>
    <span class="n">mail</span><span class="p">(</span><span class="ss">to: </span><span class="n">user</span><span class="p">.</span><span class="nf">email</span><span class="p">,</span> <span class="ss">subject: </span><span class="vi">@presenter</span><span class="p">.</span><span class="nf">subject</span> <span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="testing-before-vs-after">Testing Before vs After</h3>

<p>Before the refactor, testing required parsing rendered email output with string matching or regexes — a fragile way to verify logic.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span> <span class="s2">"return_message is urgent when due in 1 day"</span> <span class="k">do</span>
  <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Shey"</span><span class="p">)</span>
  <span class="n">book</span> <span class="o">=</span> <span class="no">Book</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Dune"</span><span class="p">,</span> <span class="ss">due_date: </span><span class="no">Date</span><span class="p">.</span><span class="nf">today</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>

  <span class="n">email</span> <span class="o">=</span> <span class="no">UserMailer</span><span class="p">.</span><span class="nf">book_due_soon</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">book</span><span class="p">).</span><span class="nf">body</span><span class="p">.</span><span class="nf">to_s</span>

  <span class="n">assert_includes</span> <span class="n">email</span><span class="p">,</span> <span class="s2">"it's due in just 1 day!"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>After the Refactor: test the presenter directly instead of the view.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span> <span class="s2">"return_message is urgent when due in 1 day"</span> <span class="k">do</span>
  <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Shey"</span><span class="p">)</span>
  <span class="n">book</span> <span class="o">=</span> <span class="no">Book</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Dune"</span><span class="p">,</span> <span class="ss">due_date: </span><span class="no">Date</span><span class="p">.</span><span class="nf">today</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>

  <span class="n">presenter</span> <span class="o">=</span> <span class="no">BookDueSoonPresenter</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">book: </span><span class="n">book</span><span class="p">)</span>

  <span class="n">assert_equal</span> <span class="s2">"Please return it as soon as possible — it's due in just 1 day!"</span><span class="p">,</span> <span class="n">presenter</span><span class="p">.</span><span class="nf">return_message</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It works the same way in controller views as it does in mailers — anywhere you have complex logic in templates.</p>

<p>Hope you found it helpful.</p>

<blockquote>
  <p><em>Update (May 2025): I later refactored this presenter once the logic started to grow. You can read about that <a href="https://shey.ca/2025/05/10/refactoring-a-presenter.html">here</a></em>.</p>
</blockquote>]]></content><author><name></name></author><summary type="html"><![CDATA[I love the presenter pattern. I use it in all my mailer views.]]></summary></entry><entry><title type="html">Daily Docker Prune with Systemd</title><link href="https://shey.ca/2025/04/10/daily-docker-prune-with-systemd.html" rel="alternate" type="text/html" title="Daily Docker Prune with Systemd" /><published>2025-04-10T01:00:00-04:00</published><updated>2025-04-10T01:00:00-04:00</updated><id>https://shey.ca/2025/04/10/daily-docker-prune-with-systemd</id><content type="html" xml:base="https://shey.ca/2025/04/10/daily-docker-prune-with-systemd.html"><![CDATA[<p>I use Docker. I used to manually <code class="language-plaintext highlighter-rouge">prune</code> Docker every few weeks to keep it from eating up all my disk. Annoyed I eventually put it in <code class="language-plaintext highlighter-rouge">cron</code>, but, have you used cron? It wasn’t fun, and I just can’t anymore with <code class="language-plaintext highlighter-rouge">cron.</code> I was complaining about it on Mastadon and the <a href="https://github.com/adam12">adam12</a> showed me a better way: use <code class="language-plaintext highlighter-rouge">systemd</code> timers to schedule the prune.
Here’s how it works: first, two files—one for the service, one for the timer.</p>

<p><strong>The service file:</strong> <code class="language-plaintext highlighter-rouge">/etc/systemd/system/docker-prune.service</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Daily Docker System Prune
<span class="nv">Wants</span><span class="o">=</span>docker.service
<span class="nv">After</span><span class="o">=</span>docker.service

<span class="o">[</span>Service]
<span class="nv">Type</span><span class="o">=</span>oneshot
<span class="nv">ExecStart</span><span class="o">=</span>/usr/bin/docker system prune <span class="nt">-f</span> <span class="nt">--filter</span> <span class="s2">"until=720h"</span>

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>multi-user.target
</code></pre></div></div>

<p><strong>The timer file:</strong> <code class="language-plaintext highlighter-rouge">/etc/systemd/system/docker-prune.timer</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Run Docker System Prune Daily

<span class="o">[</span>Timer]
<span class="nv">OnCalendar</span><span class="o">=</span>daily
<span class="nv">Persistent</span><span class="o">=</span><span class="nb">true</span>

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>timers.target
</code></pre></div></div>

<h3 id="enable-the-service-and-timer">Enable the service and timer</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl daemon-reexec
 <span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> docker-prune.timer
</code></pre></div></div>

<h3 id="confirming-its-working">Confirming it’s working</h3>

<p>List active timers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl list-timers <span class="nt">--all</span>
</code></pre></div></div>

<p>Check the logs:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>journalctl <span class="nt">-u</span> docker-prune.service
</code></pre></div></div>

<p>It runs once a day like it’s supposed to. The logs are easier to read, and the files are easier to edit. So yeah, no more cron.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I use Docker. I used to manually prune Docker every few weeks to keep it from eating up all my disk. Annoyed I eventually put it in cron, but, have you used cron? It wasn’t fun, and I just can’t anymore with cron. I was complaining about it on Mastadon and the adam12 showed me a better way: use systemd timers to schedule the prune. Here’s how it works: first, two files—one for the service, one for the timer.]]></summary></entry><entry><title type="html">No Appreciable Difference</title><link href="https://shey.ca/2025/04/09/no-appreciable-difference.html" rel="alternate" type="text/html" title="No Appreciable Difference" /><published>2025-04-09T01:00:00-04:00</published><updated>2025-04-09T01:00:00-04:00</updated><id>https://shey.ca/2025/04/09/no-appreciable-difference</id><content type="html" xml:base="https://shey.ca/2025/04/09/no-appreciable-difference.html"><![CDATA[<p>Pre-pandemic DevOps was a strange place with stranger rules, rituals, and prayers. Here’s one I said regularly—something I thought would give me faster builds and smaller images.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">RUN </span>gem <span class="nb">install </span>bundler <span class="nt">-v</span> 2.6.7 <span class="o">&amp;&amp;</span> <span class="se">\
</span>  bundle config <span class="nb">set</span> <span class="nt">--global</span> gem.rdoc <span class="nb">false</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span>  bundle config <span class="nb">set</span> <span class="nt">--global</span> gem.ri <span class="nb">false</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span>  bundle <span class="nb">install</span> <span class="nt">--no-cache</span>
</code></pre></div></div>

<p>It seemed reasonable: skip the docs, avoid the cache—build faster and ship smaller images. But I never actually tested whether these settings made a difference. <strong>Until now.</strong></p>

<p>Bundling with local caching enabled and with rdocs: <strong>87.8 seconds</strong>.</p>

<p><img src="/assets/docker/1.png" alt="build-with-caching-and-docs" /></p>

<p>Bundling without local caching enabled and without rdoc: <strong>89.7 seconds</strong>.</p>

<p><img src="/assets/docker/2.png" alt="build-without-caching-and-docs" /></p>

<p>Build time? Basically the same. There’s some variability—public internet and all—but for Ruby, there’s no appreciable difference</p>

<p>Image size? Identical in both cases: <strong>4.57GB</strong>.</p>

<p>And yes, I’m using Debian (Bookworm), which isn’t exactly slim–but that’s not the issue. The config flags I copied–the ones I believed made a difference–didn’t. They didn’t speed up the build. They didn’t shrink the image. I had assumed they would.</p>

<p>Turns out Bundler doesn’t generate docs during <code class="language-plaintext highlighter-rouge">bundle install</code>–it delegates installation to RubyGems, which skips documentation unless you explicitly ask for it. So all those extra flags like <code class="language-plaintext highlighter-rouge">gem.rdoc</code> and <code class="language-plaintext highlighter-rouge">gem.ri</code>? They don’t do anything. They just look useful.</p>

<p>Same with <code class="language-plaintext highlighter-rouge">--no-cache</code>, it sounds like it prevents .gem files from being stored in <code class="language-plaintext highlighter-rouge">/usr/local/bundle/cache</code>, but it doesn’t! That cache is created by RubyGems fetching gems before installation.So, if you want to reduce image size, you have to remove them manually.</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">RUN </span>gem <span class="nb">install </span>bundler <span class="nt">-v</span> 2.6.7 <span class="o">&amp;&amp;</span> <span class="se">\
</span>  bundle <span class="nb">install</span> <span class="nt">--no-cache</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span>  <span class="nb">rm</span> <span class="nt">-rf</span> /usr/local/bundle/cache/<span class="k">*</span>
</code></pre></div></div>

<p>And there you have it—finally, a smaller image.</p>

<p><img src="/assets/docker/3.png" alt="manual-removal" /></p>

<p>I’d been cargo-culting these flags for years. They didn’t speed anything up. They didn’t shrink the image. And now I’m here, confessing my sins. I hope this helps someone.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Pre-pandemic DevOps was a strange place with stranger rules, rituals, and prayers. Here’s one I said regularly—something I thought would give me faster builds and smaller images.]]></summary></entry><entry><title type="html">5 Gems That I Use to Simplify Development on HTTPScout</title><link href="https://shey.ca/2024/10/19/five-gems-that-make-development-easy.html" rel="alternate" type="text/html" title="5 Gems That I Use to Simplify Development on HTTPScout" /><published>2024-10-19T01:00:00-04:00</published><updated>2024-10-19T01:00:00-04:00</updated><id>https://shey.ca/2024/10/19/five-gems-that-make-development-easy</id><content type="html" xml:base="https://shey.ca/2024/10/19/five-gems-that-make-development-easy.html"><![CDATA[<p>When I was building <a href="https://httpscout.io/">HTTPScout</a>, the goal was to keep things simple. Every extra service, integration, or dependency meant more things to break, more bugs to chase, more overhead. I’ve spent enough time cleaning up after “clever” setups to know that fewer moving parts is usually the better choice. These five gems helped me stay focused on building.</p>

<h2 id="1-letter-opener-web--easy-email-debugging">1. Letter Opener Web – Easy Email Debugging</h2>

<p>Email’s annoying to debug. <a href="https://github.com/fgrehm/letter_opener_web">Letter Opener Web</a> just opens it in the browser. You see what you sent, no risk of it hitting a real inbox, no guessing on formatting. It shortens the loop and saves time.</p>

<h2 id="2-ahoy--first-party-analytics-in-rails">2. Ahoy – First Party Analytics in Rails</h2>

<p>With analytics come: privacy concerns, GDPR, and monthly bills. <a href="https://github.com/ankane/ahoy">Ahoy</a> just logs events locally. It’s simple, Rails-native, and works without bringing in another service. It gives me enough insight to improve flows without the overhead.</p>

<h2 id="3-exception-track--tracking-exceptions-without-using-yet-another-service">3. Exception Track – Tracking Exceptions Without Using Yet Another Service</h2>

<p><a href="https://github.com/rails-engine/exception-track">Exception Track</a>. It does what it says. Exceptions show up in a readable view—no parsing logs, no external service, no accounts. I don’t need anything fancy, just visibility when something breaks. Exception Track handles that and gets out of the way.</p>

<h2 id="4-invisible-captcha--blocking-bots-without-annoying-users">4. Invisible Captcha – Blocking Bots Without Annoying Users</h2>

<p>I don’t like captchas. They’re friction. With <a href="https://github.com/markets/invisible_captcha">Invisible Captcha</a>, there are no puzzles, no weird image grids. It catches bots without annoying real users. It’s great.</p>

<h2 id="5-lograge--structured-logging-ftw">5. Lograge – Structured Logging FTW</h2>

<p>I self-host HTTPScout, so I’m already reading logs. <a href="https://github.com/roidrage/lograge">Lograge</a> outputs structured JSON, which makes it easier to filter and debug with jq or grep. It’s not fancy, but it works. Also lines up well with upcoming Rails 7.2 logging changes.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>These gems save me time. They reduce noise, cut out external dependencies, and let me stay focused. Most are free. None require yet another account. If you’re trying to keep things simple, these might help.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[When I was building HTTPScout, the goal was to keep things simple. Every extra service, integration, or dependency meant more things to break, more bugs to chase, more overhead. I’ve spent enough time cleaning up after “clever” setups to know that fewer moving parts is usually the better choice. These five gems helped me stay focused on building.]]></summary></entry><entry><title type="html">Nginx and Rails: Optimize Production Config Like a Pro</title><link href="https://shey.ca/2024/10/05/nginx-config-for-rails.html" rel="alternate" type="text/html" title="Nginx and Rails: Optimize Production Config Like a Pro" /><published>2024-10-05T21:00:00-04:00</published><updated>2024-10-05T21:00:00-04:00</updated><id>https://shey.ca/2024/10/05/nginx-config-for-rails</id><content type="html" xml:base="https://shey.ca/2024/10/05/nginx-config-for-rails.html"><![CDATA[<p>I’ve always been a fan of Nginx. It’s fast, stable, feature-rich, and challenging to configure at times. I learned most of what I know about Nginx by reading other configs and making a few mistakes in production (thankfully, we have config management to handle those moments).</p>

<p>In this post, I want to share the nginx config and the customizations I use for my Rails apps. While the config isn’t perfect, it’s been reliable in production and handles traffic spikes well. There are a few quirks, but overall it’s a solid setup that has worked for me.</p>

<h3 id="rate-limiting">Rate Limiting</h3>

<p>I use a <a href="https://blog.nginx.org/blog/rate-limiting-nginx">dual-zone</a> approach to rate-limiting that doesn’t completely prevent DoS attacks but helps mitigate the risk of cascading failures during legitimate traffic surges. The per-second limit absorbs brief spikes without blocking users, while the per-minute limit manages sustained high traffic.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1">##################################</span>
<span class="c1">## Rate limiting Zone Definition</span>
<span class="c1">##################################</span>
<span class="k">limit_req_zone</span> <span class="nv">$binary_remote_addr</span> <span class="s">zone=zone_request_limit_second:10m</span> <span class="s">rate=4r/s</span><span class="p">;</span>
<span class="k">limit_req_zone</span> <span class="nv">$binary_remote_addr</span> <span class="s">zone=zone_request_limit_minute:10m</span> <span class="s">rate=120r/m</span><span class="p">;</span>


<span class="c1">##################################</span>
<span class="c1">## Rate limiting</span>
<span class="c1">##################################</span>
<span class="k">limit_req</span> <span class="s">zone=zone_request_limit_second</span> <span class="s">burst=8</span> <span class="s">nodelay</span><span class="p">;</span>
<span class="k">limit_req</span> <span class="s">zone=zone_request_limit_minute</span> <span class="s">burst=180</span> <span class="s">nodelay</span><span class="p">;</span>
<span class="k">limit_req_status</span> <span class="mi">429</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="avoiding-sending-unnecessary-traffic-to-rails">Avoiding Sending Unnecessary Traffic to Rails</h3>

<p>Since Nginx is much faster than Rails at serving static assets, I configure nginx to handle those requests directly. And by filtering out certain types of traffic at the Nginx level, I avoid sending requests to Rails that it can’t process. This keeps the app responsive, even during traffic spikes.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># Avoid sending unnecessary requests upstream to the app.</span>
<span class="k">location</span> <span class="p">~</span> <span class="sr">(\.php|\.aspx|\.asp|myadmin)</span> <span class="p">{</span>
  <span class="kn">return</span> <span class="mi">404</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1"># Serve robots.txt, favicon, etc., directly from nginx</span>
<span class="k">location</span> <span class="p">~</span> <span class="sr">^/(robots.txt|sitemap.xml.gz|favicon.ico)</span> <span class="p">{</span>
  <span class="kn">root</span> <span class="n">/home/rails/lrt/current/public</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1"># Serve precompiled assets directly</span>
<span class="k">location</span> <span class="p">~</span> <span class="sr">^/(assets)/</span> <span class="p">{</span>
  <span class="kn">gzip_static</span> <span class="no">on</span><span class="p">;</span>
  <span class="kn">root</span> <span class="n">/home/rails/lrt/current/public</span><span class="p">;</span>
  <span class="kn">expires</span> <span class="s">max</span><span class="p">;</span>
  <span class="c1"># browser cache only</span>
  <span class="kn">add_header</span> <span class="s">Cache-Control</span> <span class="s">private</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="avoid-buffering">Avoid Buffering</h3>

<p>By default, nginx is configured for lower memory usage, so if a response is too large to fit in memory, nginx will write the excess data to disk to conserve memory. This is buffering and it’s a common issue for apps that serve API requests. To avoid buffering responses, I set <code class="language-plaintext highlighter-rouge">proxy_buffers 4 256k</code>, in this case, nginx is configured to hold up to 1MB of the response in memory. When combined with <code class="language-plaintext highlighter-rouge">proxy_buffering off</code>, nginx will send responses directly to the client, improving overall response times by skipping more unecessary disk I/O.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1">#########################################################</span>
<span class="c1">## Buffers</span>
<span class="c1">#########################################################</span>
<span class="k">proxy_buffer_size</span> <span class="mi">128k</span><span class="p">;</span>
<span class="k">proxy_buffers</span> <span class="mi">4</span> <span class="mi">256k</span><span class="p">;</span>
<span class="k">proxy_busy_buffers_size</span> <span class="mi">256k</span><span class="p">;</span>
<span class="k">proxy_buffering</span> <span class="no">off</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="ssltls">SSL/TLS</h3>

<p>My TLS configuration follows <a href="https://ssl-config.mozilla.org/#server=nginx&amp;version=1.17.7&amp;config=modern&amp;openssl=1.1.1k&amp;guideline=5.7">Mozilla’s guidelines</a> for a modern and secure setup, which also allows the site to score an <a href="https://www.ssllabs.com/ssltest/analyze.html?d=httpscout.io">“A” rating</a> from SSL Labs. I’ve enabled OCSP stapling for a slight performance boost as well.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">sl_protocols</span>                    <span class="s">TLSv1.3</span><span class="p">;</span>
<span class="k">ssl_session_cache</span>               <span class="s">shared:SSL:10m</span><span class="p">;</span>
<span class="k">ssl_session_timeout</span>             <span class="mi">10m</span><span class="p">;</span>
<span class="k">ssl_certificate</span>                 <span class="n">/etc/letsencrypt/live/httpscout.io/fullchain.pem</span><span class="p">;</span>
<span class="k">ssl_certificate_key</span>             <span class="n">/etc/letsencrypt/live/httpscout.io/privkey.pem</span><span class="p">;</span>
<span class="k">ssl_session_tickets</span>             <span class="no">off</span><span class="p">;</span>
<span class="k">ssl_prefer_server_ciphers</span>       <span class="no">off</span><span class="p">;</span>
<span class="k">ssl_stapling</span>                    <span class="no">on</span><span class="p">;</span>
<span class="k">ssl_stapling_verify</span>             <span class="no">on</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="logging">Logging</h3>

<p>To make logs easier to parse and search, I’ve implemented JSON logging, which is particularly useful for troubleshooting and gathering statistics. This was something I picked up from <a href="https://www.velebit.ai/blog/nginx-json-logging/">velebit.ai</a>.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># Modified version of JSON logging: https://www.velebit.ai/blog/nginx-json-logging/</span>
<span class="k">log_format</span> <span class="s">custom</span> <span class="s">escape=json</span> <span class="s">'</span><span class="p">{</span><span class="kn">"source":</span> <span class="s">"nginx",</span> <span class="s">"time":</span> <span class="s">"</span><span class="nv">$time_iso8601</span><span class="s">",</span> <span class="s">"resp_body_size":</span> <span class="nv">$body_bytes_sent</span><span class="s">,</span> <span class="s">"host":</span> <span class="s">"</span><span class="nv">$http_host</span><span class="s">",</span> <span class="s">"address":</span> <span class="s">"</span><span class="nv">$remote_addr</span><span class="s">",</span> <span class="s">"request_length":</span> <span class="nv">$request_length</span><span class="s">,</span> <span class="s">"method":</span> <span class="s">"</span><span class="nv">$request_method</span><span class="s">",</span> <span class="s">"uri":</span> <span class="s">"</span><span class="nv">$request_uri</span><span class="s">",</span> <span class="s">"status":</span> <span class="nv">$status</span><span class="s">,</span> <span class="s">"user_agent":</span> <span class="s">"</span><span class="nv">$http_user_agent</span><span class="s">",</span> <span class="s">"referrer"</span> <span class="p">:</span> <span class="s">"</span><span class="nv">$http_referer</span><span class="s">",</span> <span class="s">"resp_time":</span> <span class="s">"</span><span class="nv">$request_time</span><span class="s">",</span> <span class="s">"upstream_addr":</span> <span class="s">"</span><span class="nv">$upstream_addr</span><span class="s">"</span><span class="err">}</span><span class="s">'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="final-thoughts">Final Thoughts</h3>

<p><strong>Update:</strong>
<a href="https://github.com/shey/osr-infra/blob/main/roles/rails-app/templates/site.j2">Full Nginx config used in production</a>. This is the same setup I use for <a href="https://httpscout.io/">HTTPScout.io</a>. Managed via Ansible, with static file handling via <code class="language-plaintext highlighter-rouge">try_files</code>, asset routing, and rate limiting.</p>

<p>Anyways, I hope you find this config helpful!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve always been a fan of Nginx. It’s fast, stable, feature-rich, and challenging to configure at times. I learned most of what I know about Nginx by reading other configs and making a few mistakes in production (thankfully, we have config management to handle those moments).]]></summary></entry></feed>