<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Rails, PostgreSQL Performance Audit and Tuning Consultant for Hire</title>
    <description>Paweł Urbanek - Full Stack Ruby on Rails web developer and consultant, specializing in performance tuning. Experienced in building scalable APIs for startups and refactoring legacy codebases. Blogging about web development related topics.</description>
    <link>https://pawelurbanek.com/</link>
    <atom:link href="https://pawelurbanek.com/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>How to Improve Rails Caching with Brotli Compression</title>
        <description>&lt;p&gt;Caching is an effective way to speed up the performance of Rails applications. However, the costs of an in-memory cache database could become significant for larger-scale projects. In this blog post, I’ll describe optimizing the Rails caching mechanism using the Brotli compression algorithm instead of the default Gzip. I’ll also discuss a more advanced technique of using in-memory cache for extreme performance bottlenecks.&lt;/p&gt;

&lt;h2 id=&quot;brotli-vs-gzip-101&quot;&gt;Brotli vs Gzip 101&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;/rails-gzip-brotli-compression&quot;&gt;one of my previous posts&lt;/a&gt;, I’ve covered how Rails leverages compression algorithms for the HTTP transport layer. Please check it out for more details on how Gzip and Brotli differ in compression ratios and performance. Here, let’s quickly recap the basics.&lt;/p&gt;

&lt;p&gt;You can use Gzip and Brotli using Ruby API wrapping underlying C libraries. To Gzip a sample JSON file and measure the compression rate, run the following code:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'json'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'zlib'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sample.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 1127380 bytes ~ 1127kb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gzipped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Zlib&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Deflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gzipped&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 167155 bytes ~ 167kb&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, a standard Gzip compression on a JSON reduced the size by ~85%.&lt;/p&gt;

&lt;p&gt;Let’s now see how to use Brotli. You have to start by installing &lt;a href=&quot;https://github.com/miyucy/brotli&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a Brotli gem&lt;/a&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gem &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;brotli&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now you can run:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'json'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'brotli'&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sample.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 1127380 bytes ~ 1127kb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;brotlied&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Brotli&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;quality: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;brotlied&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 145056 bytes ~ 145kb&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;center annotation&quot;&gt;Brotli is slow with the default quality settings, so 6 is recommended for on-the-fly compression.&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Brotli compression is ~13% better than Gzip for this sample JSON. But, &lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2020/09/cloudfront-brotli-compression/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;according to the sources&lt;/a&gt;, improvement can be as high as 25%.&lt;/p&gt;

&lt;p&gt;Another advantage of Brotli is the speed of the compression and decompression process. Based on my benchmarks, Brotli can be ~20% faster than Gzip when used for Rails cache. The overhead of the caching layer will probably not be the bottleneck of your Rails app. But, if you’re using caching extensively, then a 20% improvement might translate to a measurable global speed-up.&lt;/p&gt;

&lt;p&gt;But better compression could be even more impactful than &lt;em&gt;read/write&lt;/em&gt; performance. I’ve discussed how to reduce the costs of in-memory cache databases when caching ActiveRecord queries &lt;a href=&quot;/rails-active-record-caching&quot;&gt;in my other blog post&lt;/a&gt;. Combined with the tips on reducing the size of cache payloads, Brotli compression could lower caching infrastructure costs. Also, more space for cache means less frequent rotation of older entries, translating to better performance.&lt;/p&gt;

&lt;h2 id=&quot;how-to-use-brotli-for-rails-cache&quot;&gt;How to use Brotli for Rails cache?&lt;/h2&gt;

&lt;p&gt;By default, Rails cache uses the Gzip algorithm for payloads larger than &lt;em&gt;1kb&lt;/em&gt;. But, as we’ve discussed, Brotli offers better compression ratios and speed. You can start using Brotli in your Rails app with the help of the &lt;a href=&quot;https://github.com/pawurb/rails-brotli-cache&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-brotli-cache gem&lt;/a&gt; that I’ve recently released.&lt;/p&gt;

&lt;p&gt;It works as a proxy wrapper for standard cache stores, precompressing the payloads with Brotli instead of Gzip. I’ve measured a &lt;em&gt;20%-40%&lt;/em&gt; performance improvement in &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.cache&lt;/code&gt; depending on the underlying data store. You can check out &lt;a href=&quot;https://github.com/pawurb/rails-brotli-cache/blob/main/benchmarks/main.rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the benchmarks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After adding it to your Gemfile, you have to change a single line of config to hook it up:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/environments/production.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache_store&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsBrotliCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RedisCacheStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'REDIS_URL'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Check out the gem readme for information on using it with different cache store types.&lt;/p&gt;

&lt;p&gt;Also, remember to run &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.cache.clear&lt;/code&gt; after after first enabling it to remove previously gzipped cache entries. But watch out not to &lt;a href=&quot;https://miroslavcsonka.com/2021/03/24/rails-wiping-the-system.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;wipe your Sidekiq in the process&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Optionally, you can hook up a &lt;a href=&quot;https://github.com/pawurb/rails-brotli-cache/#use-a-custom-compressor-class&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;custom compression algorithm&lt;/a&gt;. For example, &lt;a href=&quot;https://github.com/miyucy/snappy&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google Snappy&lt;/a&gt; offers even better performance than Brotli for the cost of worse compression ratios. &lt;a href=&quot;https://github.com/SpringMT/zstd-ruby&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ZSTD by Facebook&lt;/a&gt; is also worth checking out.&lt;/p&gt;

&lt;p&gt;Gem uses an &lt;a href=&quot;https://github.com/pawurb/rails-brotli-cache/blob/main/spec/rails-brotli-cache/compatibility_spec.rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;API compatibility spec&lt;/a&gt; to ensure that it behaves exactly as the underlying data store. I’m currently using it in a few production projects, but please submit a GH issue if you notice any inconsistencies.&lt;/p&gt;

&lt;p&gt;The great news is that &lt;a href=&quot;https://github.com/rails/rails/pull/48451&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;support for custom cache compression algorithms&lt;/a&gt; has recently been merged into the Rails main branch. &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-c-new&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;A new &lt;code class=&quot;highlighter-rouge&quot;&gt;:compressor&lt;/code&gt; config option&lt;/a&gt; should be available in the upcoming Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;7.1&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;using-and-misusing-in-memory-cache-store&quot;&gt;Using and misusing in-memory cache store&lt;/h2&gt;

&lt;p&gt;I’ve mentioned that &lt;em&gt;rails-brotli-cache&lt;/em&gt; offers &lt;em&gt;~20%&lt;/em&gt; speed improvement compared to the other cache stores. But there’s a catch. Let’s consider the following benchmark:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active_support'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active_support/core_ext/hash'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'net/http'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'rails-brotli-cache'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'benchmark'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;json_uri&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://raw.githubusercontent.com/pawurb/rails-brotli-cache/main/spec/fixtures/sample.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Net&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json_uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;redis_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RedisCacheStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;brotli_redis_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsBrotliCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;redis_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;memcached_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemCacheStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;brotli_memcached_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsBrotliCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;memcached_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;file_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FileStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'/tmp'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;brotli_file_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsBrotliCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;memory_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemoryStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;brotli_memory_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsBrotliCache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;memory_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Benchmark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bm&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;redis_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;redis_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;redis_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;brotli_redis_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_redis_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_redis_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;memcached_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;memcached_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;memcached_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;brotli_memcached_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_memcached_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_memcached_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;file_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;file_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;file_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;brotli_file_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_file_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_file_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;memory_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;memory_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;memory_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;brotli_memory_cache&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;iterations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_memory_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;brotli_memory_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;center annotation&quot;&gt;Check out &lt;a class=&quot;link-grey&quot; href=&quot;https://github.com/pawurb/rails-brotli-cache/blob/main/benchmarks/main.rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the gem repo&lt;/a&gt; for a complete benchmark code&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;You should get similar results:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;                       user     system   total    real
redis_cache            1.770976 0.040232 1.811208 (2.587884)
brotli_redis_cache     1.387118 0.077257 1.464375 (2.137552)
memcached_cache        1.787665 0.058871 1.846536 (2.534051)
brotli_memcached_cache 1.368171 0.088934 1.457105 (2.043203)
file_cache             1.716816 0.055140 1.771956 (1.772132)
brotli_file_cache      1.319149 0.068127 1.387276 (1.387309)
memory_cache           0.001370 0.000117 0.001487 (0.001480)
brotli_memory_cache    1.330853 0.043904 1.374757 (1.374784)&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Here’s an ASCII chart for easier comprehension:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;redis_cache            |████████████████████
brotli_redis_cache     |████████████████

memcached_cache        |████████████████████
brotli_memcached_cache |████████████████

file_cache             |████████████████
brotli_file_cache      |███████████████

memory_cache           |
brotli_memory_cache    |███████████&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, &lt;em&gt;rails-brotli-cache&lt;/em&gt; proxy improves the performance for all the cache storage types apart from &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveSupport::Cache::MemoryStore&lt;/code&gt;. And this storage type is &lt;em&gt;~99%&lt;/em&gt; faster than all the other types. What’s going on?&lt;/p&gt;

&lt;h3 id=&quot;nested-cache-layers&quot;&gt;Nested cache layers&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveSupport::Cache::MemoryStore&lt;/code&gt; is unique because compared to other cache store types, it does not compress cached entries. Also, it keeps cached entries directly in the process RAM, so there’s no serialization, networking, or filesystem IO overhead. If we run the benchmark with in-memory compression enabled, we’ll get the following results:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;memory_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemoryStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;compress: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;memory_cache           |█████████████████
brotli_memory_cache    |███████████&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, compression causes significant overhead for all the cache store types. For some &lt;em&gt;extreme&lt;/em&gt; cases, you can use this order of magnitude better performance of uncompressed in-memory cache-store. The tradeoff will be higher RAM usage of your Ruby processes, but &lt;em&gt;99%&lt;/em&gt; performance improvement could be worth it.&lt;/p&gt;

&lt;p&gt;To do it, you can wrap your standard Rails cache into a custom in-memory store like this:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/application.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vg&quot;&gt;$memory_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MemoryStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;and now in your bottleneck endpoint:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/controller/users_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProductsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cached_products-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;cached_products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vg&quot;&gt;$memory_cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;race_condition_ttl: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seconds&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FetchIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;json: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cached_products&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In the above example, we’re using two layers of caching. The outer layer using &lt;code class=&quot;highlighter-rouge&quot;&gt;$memory_store&lt;/code&gt; in-memory cache is set to expire every 30 seconds. It means that our underlying standard cache store will be called significantly less often. As discussed, the performance of the caching layer for most of the requests will be &lt;em&gt;~99%&lt;/em&gt; better. For endpoints under heavy load, configuring &lt;code class=&quot;highlighter-rouge&quot;&gt;race_condition_ttl&lt;/code&gt; will also reduce the number of redundant cache refresh calls.&lt;/p&gt;

&lt;p&gt;This example might seem a bit convoluted. But for bottleneck endpoints that serve identical data to multiple clients, similar &lt;em&gt;hacks&lt;/em&gt; could measurably reduce the load on your Redis/Memcache database and improve performance. But remember that it’s easy to misuse this technique and bloat the app’s RAM usage.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Replacing a standard Gzip compression with &lt;em&gt;rails-brotli-cache&lt;/em&gt; gem is a relatively simple change that might result in a global speed-up for your Rails app. Another benefit is that it could help you reduce the costs of the in-memory cache database thanks to better compression. I invite you to give it a try. Feedback and PRs on how to improve the gem are welcome.&lt;/p&gt;
</description>
        <pubDate>Tue, 12 Sep 2023 08:36:01 +0200</pubDate>
        <link>https://pawelurbanek.com/rails-brotli-cache</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-brotli-cache</guid>
      </item>
    
      <item>
        <title>Five Easy to Miss Performance Fixes for Rails Apps</title>
        <description>&lt;p&gt;Improving the performance of a Rails application can be a challenging and time-consuming task. However, there are some config tweaks that are often overlooked but can make a significant difference in response times. In this tutorial, I will focus on a few &lt;em&gt;“quick &amp;amp; easy”&lt;/em&gt; fixes that can have an immediate impact on the speed of your Rails app.&lt;/p&gt;

&lt;h2 id=&quot;eliminate-request-queue-time&quot;&gt;Eliminate request queue time&lt;/h2&gt;

&lt;p&gt;Request queue time means that your web servers don’t have enough capacity to process the incoming traffic. Unless you’re using a PAAS platform like Heroku, you have to add an extra NGINX header to enable request queuing data in the logs:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;&lt;span class=&quot;k&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Request-Start&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;t=&lt;/span&gt;$&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;msec&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This config will enable any popular APM monitoring tools to report the current queue time. Without it, it’s impossible to know the real backend response time as experienced by your users.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;NewRelic APM Request Queue metric&quot; title=&quot;NewRelic APM Request Queue metric&quot; loading=&quot;lazy&quot; src=&quot;/assets/newrelic-request-queue-76275d79102025d4c83e885510dc25df7612e4da798efe06f6a87d89628c202f.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;Correctly configured NewRelic APM should display &lt;code&gt;Request Queuing&lt;/code&gt; metric&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The presence of queue time is both good and bad news. Your consumers experience longer response times, but resolving it by spinning up more server instances is usually possible. Before throwing money at a dozen new Heroku dynos, double-check if you cannot extract more throughput without increasing the cost.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#recommended-default-puma-process-and-thread-configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;These Heroku docs&lt;/a&gt; recommend values of &lt;code class=&quot;highlighter-rouge&quot;&gt;WEB_CONCURRENCY&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;RAILS_MAX_THREADS&lt;/code&gt; for different dyno types. It’s worth noting the &lt;code class=&quot;highlighter-rouge&quot;&gt;Performance-M&lt;/code&gt; dynos offer the worst cost/metrics and should usually be replaced by a single &lt;code class=&quot;highlighter-rouge&quot;&gt;Performance-L&lt;/code&gt; or a fleet of &lt;code class=&quot;highlighter-rouge&quot;&gt;Standard-2X&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I usually try to squeeze enough Puma workers so that RAM usage never spikes above 80%. A relatively simple way to decrease memory usage and prevent leaks is to use &lt;a href=&quot;https://github.com/jemalloc/jemalloc&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Jemalloc&lt;/a&gt; instead of the default memory allocator. I’ve seen memory usage drop by 15-20% after adding Jemalloc. It usually allowed us to safely add one more Puma worker for the same server/dyno type and increase the app’s throughput. You can check out my other blog post for more info on &lt;a href=&quot;/2018/01/15/limit-rails-memory-usage-fix-R14-and-save-money-on-heroku/&quot;&gt;how to use Jemalloc in Rails apps&lt;/a&gt; and other tips on reducing memory usage.&lt;/p&gt;

&lt;h2 id=&quot;enable-cache-and-redis-connection-pool&quot;&gt;Enable cache and Redis connection pool&lt;/h2&gt;

&lt;p&gt;Unless you’re on the newest Rails, your app’s caching layer may be misconfigured. Production Rails apps often use Redis or Memcache as their in-memory cache backend. But, the default config of the cache client is single-threaded. A default support for connection pool &lt;a href=&quot;https://github.com/rails/rails/pull/45111&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;was added around a year ago&lt;/a&gt;. So, if you’re using Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt; 6.1&lt;/code&gt; with a Puma web server, multiple Puma threads compete for a single cache client connection.&lt;/p&gt;

&lt;p&gt;You can enable a connection pool for your caching client with similar config options:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/production.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache_store&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:mem_cache_store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cache.example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                     &lt;span class=&quot;ss&quot;&gt;pool: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;size: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;timeout: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;If your app uses Redis directly, it could have the same problem. It’s a popular pattern to expose Redis client via a global variable:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vg&quot;&gt;$redis&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;REDIS_URL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vg&quot;&gt;$redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;key-name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But it means any interaction with it will be throttled in multithreaded processes like Puma or Sidekiq. Redis usually responds in &lt;em&gt;~1ms&lt;/em&gt;, but if you’re using it extensively, dozens of blocked calls could add up to a measurable overhead.&lt;/p&gt;

&lt;p&gt;You can resolve it by leveraging a &lt;a href=&quot;https://github.com/mperham/connection_pool&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;connection-pool gem&lt;/a&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vg&quot;&gt;$redis_pool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ConnectionPool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;size: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RAILS_MAX_THREADS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;REDIS_URL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vg&quot;&gt;$redis_pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;key-name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;fine-tune-database-connections-pool&quot;&gt;Fine-tune database connections pool&lt;/h2&gt;

&lt;p&gt;Balancing the number of web servers and background worker threads with the database pool and max connections can be challenging. &lt;code class=&quot;highlighter-rouge&quot;&gt;pool&lt;/code&gt; config from &lt;code class=&quot;highlighter-rouge&quot;&gt;config/database.yml&lt;/code&gt; file determines how many database connections can be opened by a single Ruby &lt;em&gt;process&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you’re using a multithreaded process like Puma or Sidekiq, you need to make sure that the value of &lt;code class=&quot;highlighter-rouge&quot;&gt;pool&lt;/code&gt; is at least equal to the maximum number of threads. Otherwise, your app could start raising &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord::ConnectionTimeoutError&lt;/code&gt; if it cannot establish a database connection for the specified &lt;code class=&quot;highlighter-rouge&quot;&gt;timeout&lt;/code&gt;. The sum of possible connections should not be larger than the &lt;code class=&quot;highlighter-rouge&quot;&gt;max_connections&lt;/code&gt; PostgreSQL setting. But you cannot set &lt;code class=&quot;highlighter-rouge&quot;&gt;max_connection&lt;/code&gt; to any arbitrary value. Multiple concurrent connections will increase the load on a database, so its specs must be adjusted accordingly.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Abot AWS Cloudwatch dashboard requests detected by Scout APM&quot; title=&quot;Abot AWS Cloudwatch dashboard requests detected by Scout APM&quot; src=&quot;/assets/abot-cloudwatch-dashboard-7099845bc540346474947ae0098044c73eb6db7a6586d1cd73d7633a8c9dec7a.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;AWS RDS offers powerful insights and customizable CloudWatch alerts for PostgreSQL database.&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;It’s possible to oversee &lt;code class=&quot;highlighter-rouge&quot;&gt;pool&lt;/code&gt; misconfiguration. For example, threads could be waiting for database access for a period shorter than &lt;code class=&quot;highlighter-rouge&quot;&gt;timeout&lt;/code&gt;, adding performance overhead but never raising the error.&lt;/p&gt;

&lt;p&gt;You must be extra careful when using a &lt;code class=&quot;highlighter-rouge&quot;&gt;load_async&lt;/code&gt; API introduced in Rails 7. This is because it schedules &lt;em&gt;threads within threads&lt;/em&gt; and exhausts more database connections. You can read about it in more detail in &lt;a href=&quot;/rails-load-async&quot;&gt;my other blog post&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;customize-postgresql-config&quot;&gt;Customize PostgreSQL config&lt;/h2&gt;

&lt;p&gt;Depending on what PostgreSQL provider your app uses, you might be able to tweak the database config. More extensive control is another reason why I recommend &lt;a href=&quot;/heroku-postgres-aws-rds&quot;&gt;migrating the Heroku database to RDS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your PostgreSQL is handling larger datasets, tweaking the default config will likely increase SQL performance. A few examples:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;random_page_cost&lt;/code&gt; is set to &lt;code class=&quot;highlighter-rouge&quot;&gt;4.0&lt;/code&gt; by default. It determines how likely a query planner is to use an index scan instead of a sequential scan. The default value of &lt;code class=&quot;highlighter-rouge&quot;&gt;4.0&lt;/code&gt; originates from IO read speed of HDD disks. It is no longer relevant when databases use much faster SSDs for storage. Setting this value between &lt;code class=&quot;highlighter-rouge&quot;&gt;1.1-2.0&lt;/code&gt; will make better use of database indexes and reduce CPU-intensive sequential scans.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;work_mem&lt;/code&gt; determines how much RAM is available per active connection. The default value is &lt;code class=&quot;highlighter-rouge&quot;&gt;4096kb&lt;/code&gt;. Depending on your app’s usage patterns, this setting might throttle SQL queries doing more complex in-memory sorting and hash table operations. This value has to be tweaked carefully because it could cause out-of-memory issues for a database. But, ensuring that each connection has enough RAM will likely increase the performance of more complex queries.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;shared_buffers&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;effective_cache_size&lt;/code&gt; settings determine how much database RAM is delegated for caching. Performant databases should read &lt;em&gt;~99%&lt;/em&gt; of data from the in-memory cache. You can use &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras#cache_hit&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras &lt;code class=&quot;highlighter-rouge&quot;&gt;cache_hit&lt;/code&gt;&lt;/a&gt; to inspect your app’s database cache usage. For some cases increasing the default value is likely to result in a speedup for the database layer.&lt;/p&gt;

&lt;h2 id=&quot;optimize-http-layer-config&quot;&gt;Optimize HTTP layer config&lt;/h2&gt;

&lt;p&gt;All the previous tips are related to backend layer performance. But, a typical web app issues dozens of requests on initial load. And only a few are backend-related, i.e., website HTML, API calls, etc. The majority of requests are static assets, JavaScript libraries, and images. Therefore, optimizing the frontend-related requests can have a much greater effect on the perceivable performance than fine-tuning the backend.&lt;/p&gt;

&lt;p&gt;Check out my other blog post for more detailed tips on &lt;a href=&quot;/frontend-performance-optimization&quot;&gt;optimizing frontend performance&lt;/a&gt;. But, since I’ve promised &lt;em&gt;“quick and easy”&lt;/em&gt; fixes, let’s cover two potentially most impactful frontend config tweaks.&lt;/p&gt;

&lt;h3 id=&quot;client-side-caching&quot;&gt;Client-side caching&lt;/h3&gt;

&lt;p&gt;Correctly configuring client-side caching could be the most critical frontend optimization. There’s no better way to speed up the HTTP request than not to trigger it. But I’ve seen it misconfigured in multiple production apps. Rails have a great mechanism to leverage client-side caching, i.e., &lt;em&gt;MD5 digest&lt;/em&gt;. In the production environment, the &lt;code class=&quot;highlighter-rouge&quot;&gt;application.js&lt;/code&gt; file becomes &lt;code class=&quot;highlighter-rouge&quot;&gt;application-523bf7...95c125.js&lt;/code&gt;. The random suffix is generated based on the file contents, so it is guaranteed to change if the file changes. You must add the correct &lt;code class=&quot;highlighter-rouge&quot;&gt;cache-control&lt;/code&gt; header to make sure that once downloaded, the file will persist in the browser cache:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cache-control: public, max-age&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;31536000, immutable&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;immutable&lt;/code&gt; parameter ensures that the cache is not cleared when the user explicitly refreshes the website on the Chrome browser.&lt;/p&gt;

&lt;p&gt;You have to enable it in your Rails production config:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/production.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;static_cache_control&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'public, max-age=31536000, immutable'&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;public_file_server&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;'Cache-Control'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'public, max-age=31536000, immutable'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;'Expires'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_formatted_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:rfc822&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Or if you’re using NGINX as a reverse proxy, you can use the following directive:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;&lt;span class=&quot;k&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.(?:ico|css|js|gif|jpe?g|png|woff2)&lt;/span&gt;$ &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;add_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Cache-Control&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;public,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;max-age=31536000,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;immutable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kn&quot;&gt;try_files&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$uri&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;404&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I’ve seen apps using &lt;code class=&quot;highlighter-rouge&quot;&gt;Etag&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;Last-Modified&lt;/code&gt; headers instead of &lt;code class=&quot;highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt;. &lt;code class=&quot;highlighter-rouge&quot;&gt;Etag&lt;/code&gt; is also generated based on the file contents, but the client has to talk to the server to confirm that the cached version is still correct. It means that on every page visit, the browser has to issue a request to validate its cache contents and wait for &lt;code class=&quot;highlighter-rouge&quot;&gt;304 Not Modified&lt;/code&gt; response. This  completely unnecessary network roundtrip can be avoided by adding a &lt;code class=&quot;highlighter-rouge&quot;&gt;Cache-Control&lt;/code&gt; header.&lt;/p&gt;

&lt;h3 id=&quot;use-cloudflare-cdn&quot;&gt;Use Cloudflare CDN&lt;/h3&gt;

&lt;p&gt;Using CloudFlare for your DNS together with proxying enabled could be the simplest way to get most of your frontend-related config in order. It will be especially impactful if you’re using Heroku, which still does not support HTTP2 or even native Gzip compression… (╯°□°)╯︵ ┻━┻&lt;/p&gt;

&lt;p&gt;Client-side caching with Cloudflare proxy will result in your static assets getting cached in CDN edge locations close to your end users. Subsequent requests for the same assets will not be triggered because data is now cached in the browser.&lt;/p&gt;

&lt;p&gt;Please remember to use &lt;strong&gt;Respect existing headers&lt;/strong&gt; for &lt;strong&gt;Browse Cache TLL&lt;/strong&gt; setting to regain more fine-tuned control and avoid stale cache issues.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Metamask ERC20 approval limit UI&quot; title=&quot;Metamask ERC20 approval limit UI&quot; loading=&quot;lazy&quot; src=&quot;/assets/cloudflare-cache-headers-19e3d6765942b0527ce57f4fd6459728997909345fff820dff0f449b3c32fe8f.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In addition to global CDN CloudFlare will enable to following benefits for your app:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;automatic gzip/brotli compression&lt;/li&gt;
  &lt;li&gt;HTTP2 support&lt;/li&gt;
  &lt;li&gt;DDOS protection&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://web.dev/signed-exchanges/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Signed Exchanges SXGs&lt;/a&gt; support that can benefit Largest Contentful Paint (LCP) metric and SEO (available only for paid plans)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And many features more.&lt;/p&gt;

&lt;p&gt;You can use &lt;a href=&quot;https://pagespeed.web.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Page Speed insights&lt;/a&gt; and &lt;a href=&quot;https://www.webpagetest.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WebPageTest&lt;/a&gt; to get a quick overview of your app’s fronted performance.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Optimizing the mentioned configs is a great starting point for in-house performance tuning. They are likely to globally impact the performance metrics, so the development time spent tweaking them will probably yield measurable benefits.&lt;/p&gt;
</description>
        <pubDate>Tue, 09 May 2023 05:36:01 +0200</pubDate>
        <link>https://pawelurbanek.com/rails-performance-fixes</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-performance-fixes</guid>
      </item>
    
      <item>
        <title>How to Freeze ETH and ERC20 Tokens using Smart Contract</title>
        <description>&lt;p&gt;I’m still the kind of person that buys $500 worth of Dogecoin, and one day later, panic sells with a 30% loss. That’s why, I’ve developed Ethereum smart contracts that make speculating on cryptocurrencies less stressful. &lt;a href=&quot;https://github.com/pawurb/Locker&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;em&gt;Locker&lt;/em&gt; smart contracts&lt;/a&gt; allow me to freeze crypto assets and only release them when enough time has passed or the price has reached a predefined value. This approach sacrifices liquidity for peace of mind, and for me, that’s a perfect tradeoff! In this article, I’ll describe this project and the risks associated with using it or basically any Smart Contract.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: The information provided in this blog post is for educational purposes only and should not be treated as investment advice.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;decentralized-trust&quot;&gt;Decentralized trust&lt;/h2&gt;

&lt;p&gt;The crypto scene is ripe with scams and exploits siphoning billions in user funds. Despite that, I kind of see it as the only tech that can be &lt;em&gt;“trusted”&lt;/em&gt;. Ethereum Smart Contracts are literally the only existing technology guaranteed to work as programmed.&lt;/p&gt;

&lt;p&gt;Of all the popular smart contract technologies, Ethereum is the only one that enables a &lt;em&gt;standard user&lt;/em&gt; to run a full node.  On the other hand, all the &lt;em&gt;“Ethereum killers”&lt;/em&gt; network nodes are either centralized or require processing power and dev ops expertise accessible only to well-funded entities. As a result, the network consensus is governed by a few centralized actors rather than the decentralized community. You can check this tutorial for more info on &lt;a href=&quot;/ethereum-node-aws&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;how to run your own Ethereum full node&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Obviously, &lt;em&gt;“I’m in it for the technology”&lt;/em&gt;. I’ve become attracted to crypto as a tech rather then casino after getting all my bank funds repeatedly locked. It is enough to send one transfer to Revolut, too many to trigger a complete lockdown of all my accounts and credit cards. &lt;em&gt;“Risky transaction…“&lt;/em&gt; they said. Getting my funds unfrozen usually takes ~30min on a bank support line. But the idea that all my finances can be locked at a whim of a single centralized entity is disturbing…&lt;/p&gt;

&lt;p&gt;You’ve probably heard about Canadian anti-vax trucker supporters getting their accounts frozen without any judicial order. In Poland, we’ve also had our taste of a digital prison. People protesting against anti-abortion laws got their bank accounts blocked for &lt;em&gt;“breaking the pandemic sanitary regulations”&lt;/em&gt;. Or the case of Ukrainian refugees seeing their EUR account balances automatically converted to UAH with a special exchange rate spread &lt;em&gt;“because war”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Diversifying a part of funds in a layer that’s unreachable by a state-level 3rd party seems like a reasonable idea. Since this revelation, I’ve started seeing the crypto price ups and downs as a &lt;em&gt;“tax for safety”&lt;/em&gt; rather than a chance for speculation.&lt;/p&gt;

&lt;p&gt;So much for the digression. Let’s move on to the risks of using the Locker or basically any smart contract.&lt;/p&gt;

&lt;h2 id=&quot;can-you-trust-smart-contracts&quot;&gt;Can you trust Smart Contracts?&lt;/h2&gt;

&lt;p&gt;Different layers of trust could be broken and cause the loss of tokens you deposit into smart contracts.&lt;/p&gt;

&lt;h3 id=&quot;bugs-and-deliberate-scams&quot;&gt;Bugs and deliberate scams&lt;/h3&gt;

&lt;p&gt;To start with, the contract author could be a scammer or an clumsy blockchain dev. So the &lt;em&gt;Locker&lt;/em&gt; source code could contain a sneaky loophole or bug causing the loss of your funds. The open-source nature of Ethereum Smart Contracts is the best remedy for these risks.&lt;/p&gt;

&lt;p&gt;Before you deploy the contract or interact with an already live instance, you can always analyze the source code. But currently, it requires a significant dose of expertise. &lt;em&gt;Locker&lt;/em&gt; smart contracts are &lt;em&gt;relatively&lt;/em&gt; simple, maxing out at 200 LOC with minimal external dependencies. But, most Defi projects are endless blobs of code that are impossible to analyze single-handedly.  A potential revolution in the safety of Smart Contracts could be the advent of AI. ChatGPT seems to be proficient &lt;a href=&quot;https://twitter.com/jconorgrogan/status/1635695064692273161&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;in analyzing security issues of Ethereum smart contracts&lt;/a&gt;. An AI-based service listing potential risks of any smart contract interaction (maybe even integrated into your wallet!) could be just around the corner.&lt;/p&gt;

&lt;p&gt;The contract’s external dependencies and logic introduce another risk factor. In the case of &lt;em&gt;Locker&lt;/em&gt;, it &lt;em&gt;“trusts”&lt;/em&gt; the provided ChainLink oracles to report the correct price of the target token. The user selects an oracle address himself when configuring the deposit. The only risk is that oracles could be abandoned and not update the price correctly. In that case, your tokens would be released only after the lockup period.&lt;/p&gt;

&lt;p&gt;In the case of more complex smart contracts, external risk factors are often significant. For example, the creator of an ERC20 token could grant himself the power to print unlimited units and tank the price at your cost. Given the complexity of Defi smart contracts spotting similar risks is non-trivial. Hopefully, AI will be able to help standard users make more sense of these issues.&lt;/p&gt;

&lt;h3 id=&quot;frontend-interface&quot;&gt;Frontend interface&lt;/h3&gt;

&lt;p&gt;Another layer of trust is how one can interact with the contract instance. &lt;em&gt;Locker&lt;/em&gt; does not offer any front-end UI. You have to interact with it directly on Etherscan or use programmatic API. Interacting with any Defi project featuring custom frontend is a risk factor. Exploits targeting frontend layer of the smart contracts stack could alter where your funds are sent. Or malicious browser extension can change the frontend code of an otherwise trustworthy web3 domain.&lt;/p&gt;

&lt;p&gt;The best way to reduce the risk of interacting with any smart contracts is to use minimum ERC20 approval limits. Metamask has recently improved its UX by allowing users to input approval limits manually. Before that, the approval limits were implicitly unlimited, allowing attackers to withdraw all your assets.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Metamask ERC20 approval limit UI&quot; title=&quot;Metamask ERC20 approval limit UI&quot; loading=&quot;lazy&quot; src=&quot;/assets/metamask-approve-limit-0614b670fbda10262acf090fae9b2fa42baa013c857f1b3143ad3c9adb3b2a95.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;Metamask UI for custom ERC20 approval limit&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Also, using a hardware wallet like &lt;a href=&quot;https://www.ledger.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ledger&lt;/a&gt; or &lt;a href=&quot;https://trezor.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Trezor&lt;/a&gt; can prevent a range of crypto exploits.&lt;/p&gt;

&lt;h3 id=&quot;government-censorship&quot;&gt;Government censorship&lt;/h3&gt;

&lt;p&gt;A government could, in theory, censor any smart contract address. So far, the most famous case of government sanctioning a piece of autonomous software was &lt;a href=&quot;https://home.treasury.gov/news/press-releases/jy0916&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Tornado Cash&lt;/a&gt;. Apparently, you can also &lt;a href=&quot;https://www.coindesk.com/policy/2023/02/15/tornado-cash-developer-to-stay-jailed-as-dutch-trial-continues/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;go to jail for writing open-source code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fight against decentralized finance may intensify. For example, &lt;a href=&quot;https://www.coindesk.com/consensus-magazine/2023/03/29/the-eus-smart-contract-kill-switch-mandate-wont-kill-crypto/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the EU is currently brewing new restrictions for Smart Contracts&lt;/a&gt;. But, despite sanctions from the US government, the &lt;a href=&quot;https://etherscan.io/address/0x910cbd523d972eb0a6f4cae4618ad62622b39dbf&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Tornado Cash smart contracts are still regularly used&lt;/a&gt;. It shows that cryptocurrencies excel at their core value proposition of being uncensorable even by government-level actors.&lt;/p&gt;

&lt;p&gt;All the centralized elements of Tornado Cash like frontend interface, domain, and GitHub repos were succesfully taken down. But it’s still possible to access Tornado Cash immutable smart contracts using proprietary Ethereum full node or censorship-resistant relay providers.&lt;/p&gt;

&lt;h3 id=&quot;user-error&quot;&gt;User error&lt;/h3&gt;

&lt;p&gt;Any smart contract interaction is risky. There’s no &lt;em&gt;edit/undo&lt;/em&gt; button for incorrect transactions. Ask the &lt;a href=&quot;https://cointelegraph.com/news/blockchain-enthusiast-allegedly-losses-500k-by-sending-weth-to-contract-address&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;guy who burned 500k USD&lt;/a&gt; by incorrectly unwrapping WETH. If you misuse Smart Contract and lose funds, even Metamask Twitter Support will be unable to help you.&lt;/p&gt;

&lt;p&gt;Now that we’ve briefly covered the risks, let’s move on to the Locker smart contracts.&lt;/p&gt;

&lt;h2 id=&quot;locker-smart-contracts&quot;&gt;Locker smart contracts&lt;/h2&gt;

&lt;p&gt;There are four different flavours: &lt;code class=&quot;highlighter-rouge&quot;&gt;Locker&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;LockerETH&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;LockerPriv&lt;/code&gt;, and &lt;code class=&quot;highlighter-rouge&quot;&gt;LockerETHPriv&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;locker&quot;&gt;Locker&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/pawurb/Locker#lockersol&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Locker&lt;/code&gt;&lt;/a&gt; can be used to freeze any ERC20 token and is accessible by any account. Your funds will be deposited into the same contract address as other users. But, each user can only withdraw his tokens. The benefit is that you don’t have to deploy the contract instance yourself. ETH can be held by this contract only in its ERC20 format, i.e., as WETH. To use price conditions for release, you must provide a correct  &lt;a href=&quot;https://docs.chain.link/data-feeds/price-feeds/addresses&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ChainLink price oracle address&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Warning]&lt;/strong&gt; Do not transfer ERC20 tokens directly to this contract instance or they will be lost! You have to use a dedicated &lt;code class=&quot;highlighter-rouge&quot;&gt;deposit&lt;/code&gt; method. Also, do not use this contract for storing rebasing tokens like stETH or rETH. Stored token balance is determined once when depositing the token. It means that rebased reward will get stuck in the contract forever.&lt;/p&gt;

&lt;h3 id=&quot;lockereth&quot;&gt;LockerETH&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/pawurb/Locker#lockerethsol&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;LockerETH&lt;/code&gt;&lt;/a&gt; works the same as &lt;code class=&quot;highlighter-rouge&quot;&gt;Locker&lt;/code&gt; but only for standard ETH instead of ERC20 tokens. It uses a predefined ChainLink ETH/USD price oracle. It’s also publicly accessible.&lt;/p&gt;

&lt;h3 id=&quot;lockerpriv&quot;&gt;LockerPriv&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/pawurb/Locker#lockerprivsol&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;LockerPriv&lt;/code&gt;&lt;/a&gt; works as your private vault for ERC20 tokens. Only the deployer account can interact and deposit funds into it. The downside is that you have to pay the costs of releasing it. With Mainnet gas prices at 20 gwei and the price of ETH hovering around $2000 it costs ~$100. Alternatively, you can use one of the L2 layers to reduce the deployment cost. For example, on &lt;a href=&quot;https://arbitrum.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Arbitrum L2&lt;/a&gt; it costs ~$5 to deploy an instance of this Smart Contract.&lt;/p&gt;

&lt;h3 id=&quot;lockerethpriv&quot;&gt;LockerETHPriv&lt;/h3&gt;

&lt;p&gt;As you can probably guess &lt;a href=&quot;https://github.com/pawurb/Locker#lockerethprivsol&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;LockerETHPriv&lt;/code&gt;&lt;/a&gt;, is a private vault for storing a standard ETH. Just like in the case of &lt;code class=&quot;highlighter-rouge&quot;&gt;LockerPriv&lt;/code&gt; you have to deploy it yourself.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;I hope the post has helped highlight the risks of using Smart Contracts. Please remember that crypto and Defi is risky, and you can easily lose all your funds.&lt;/p&gt;

&lt;p&gt;But cryptocurrencies are here to stay. UX and general appeal is only about to improve. The worst-case scenario is that hostile governments might slow adoption for a few years until there’s a shift in power. So safely stashing your coins could be a strategy to wait out turbulent times.&lt;/p&gt;
</description>
        <pubDate>Wed, 19 Apr 2023 00:36:01 +0200</pubDate>
        <link>https://pawelurbanek.com/freeze-erc20-eth</link>
        <guid isPermaLink="true">https://pawelurbanek.com/freeze-erc20-eth</guid>
      </item>
    
      <item>
        <title>How to Find, Debug and Fix N+1 Queries in Rails</title>
        <description>&lt;p&gt;Fixing N+1 issues is often the lowest-hanging fruit in optimizing a Rails app performance. However, for non-trivial cases choosing a correct fix could be challenging. Incorrectly applied eager loading does not work or even worsens response times. In this blog post, I describe tools and techniques I use to simplify resolving N+1 issues.&lt;/p&gt;

&lt;h2 id=&quot;how-to-simulate-n1-issues-in-development&quot;&gt;How to simulate N+1 issues in development&lt;/h2&gt;

&lt;p&gt;I doubt the Rails blogosphere needs yet another introduction to N+1 queries. You can check out &lt;a href=&quot;/rails-n-1-queries&quot;&gt;my previous post&lt;/a&gt; for a quick recap.&lt;/p&gt;

&lt;p&gt;Instead, let’s discuss how to establish a repeatable routine for spotting N+1 issues before they start manifesting in production. It’s common for developers to work with a minimal local data set. Unfortunately, working with a database resembling production is usually not possible. But seeding local data for your test user to mimic production sizes could be a perfect compromise.&lt;/p&gt;

&lt;p&gt;Hundreds of test objects will not be enough to simulate slow SQL queries. PostgreSQL usually needs tens of thousands of table rows for any SQL-layer slowdown to manifest. But N+1 query issues are not related to absolute collection sizes but to how you use and misuse ActiveRecord ORM. A dozen objects are enough to trigger N+1 issues and make them detectable during development.&lt;/p&gt;

&lt;p&gt;A simple way to quickly populate your local data is to use a &lt;a href=&quot;https://github.com/thoughtbot/factory_bot&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FactoryBot gem&lt;/a&gt;. You may already have used it in your project but did not consider leveraging it to seed development env. To do it you have to include the gem in &lt;code class=&quot;highlighter-rouge&quot;&gt;development&lt;/code&gt; group of your &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;. Now in the Rails console:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'factory_bot'&lt;/span&gt;

&lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;FactoryBot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;your_test_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Of course, the exact implementation will be unique for each project. But leveraging existing factories should simplify the process compared to writing seed scripts from scratch.&lt;/p&gt;

&lt;p&gt;Another benefit of working with a larger local collection is that you’ll spot places likely to benefit from pagination. Before endless lists, bring your production servers to a crawl…&lt;/p&gt;

&lt;h2 id=&quot;how-to-analyze-and-debug-local-n1-issues&quot;&gt;How to analyze and debug local N+1 issues&lt;/h2&gt;

&lt;p&gt;Now that you have a decent local dataset, you can start digging deeper. &lt;a href=&quot;https://github.com/flyerhzm/bullet&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;bullet&lt;/a&gt; gem is a popular tools for reporting N+1 issues. However, its log output is a bit noisy or even illegible for larger projects. I’ve also seen it report many false positives so extracting actionable data it could be challenging. Instead, I usually default to the good ol’ &lt;a href=&quot;https://github.com/MiniProfiler/rack-mini-profiler&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rack-mini-profiler gem&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;rack-mini-profiler reporting N+1 queries&quot; title=&quot;rack-mini-profiler reporting N+1 queries&quot; loading=&quot;lazy&quot; src=&quot;/assets/rack-mini-profiler-n-queries-38885c333e0c7f8e90d8ac63b4ff2eafa2b8494b7de62f2f4c6df356f72e6070.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;rack-mini-profiler reporting the number of SQL queries&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The popup window shows the number of SQL queries executed per request. It even works with XHR requests. So, if you’re using a SPA framework, you can check quickly how many queries are triggered by each API endpoint.&lt;/p&gt;

&lt;p&gt;Another great feature is that you can see the exact line of code that triggered each query:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;rack-mini-profiler showing query backtraces&quot; title=&quot;rack-mini-profiler showing query backtraces&quot; loading=&quot;lazy&quot; src=&quot;/assets/rack-mini-profiler-traces-f9ce7bcbcde8c380956649c8884416ded9b1bb00597af55374ceb44fe03a20ac.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For many more straightforward cases, this data is enough to track the missing eager loading and validate the fix. But it’s normal for legacy projects to trigger thousands of queries per view. If that is the case making sense of rack-mini-profiler endless backtraces data could be overwhelming. That why I’ve recently developed a way to simplify this process.&lt;/p&gt;

&lt;p&gt;After expanding the traces data, click &lt;em&gt;CMD+A&lt;/em&gt; and &lt;em&gt;CMD+C&lt;/em&gt; to copy it into the clipboard. Now create a new empty text file and paste clipboard contents. I recommend using &lt;em&gt;vim&lt;/em&gt; because &lt;em&gt;modern&lt;/em&gt; text editors might not handle larger clipboard sizes. Now you can run the following bash script:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;sort &lt;/span&gt;traces.txt | &lt;span class=&quot;nb&quot;&gt;uniq&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sort&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-bgr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; output.txt&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;It groups and counts the unique lines, and pipes output into a new text file. Now on top of this file, you can see which kinds of queries and method execution hits were repeated most often:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt; ...
 119 Reader
 118 Executing action: home
 101 app/controllers/web/static_pages_controller.rb:7:in `home'
 101 SELECT &quot;teams&quot;.* FROM &quot;teams&quot; WHERE &quot;teams&quot;.&quot;id&quot; = $1 LIMIT $2;
 100 app/controllers/web/static_pages_controller.rb:7:in `map'
 ...&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;It’s usually enough data to spot precisely where the worse N+1 issues originate from and to validate if the fix resolved the problem.&lt;/p&gt;

&lt;h2 id=&quot;use-rails-pg-extras-to-dissect-n1-queries&quot;&gt;Use rails-pg-extras to dissect N+1 queries&lt;/h2&gt;

&lt;p&gt;As you can see, the rack-mini-profiler works for many standard scenarios. However, it’s sometimes a lot of work to simulate certain edge cases in the app’s UI. Also, the workflow of manually copying, parsing, and analyzing log outputs is far from ideal. So, I’ve recently released a small tool that helps me iterate on N+1 issues faster.&lt;/p&gt;

&lt;p&gt;My &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras gem&lt;/a&gt; has been around for a while. It includes an array of helper methods for analyzing the internals of your PostgreSQL database. &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras#measure_queries&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;measure_queries&lt;/code&gt; method&lt;/a&gt; is the newest addition to the lib’s API. It leverages &lt;a href=&quot;https://guides.rubyonrails.org/active_support_instrumentation.html#sql-active-record&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ActiveSupport instrumentation&lt;/a&gt; to display queries executed by running any provided Ruby snippet:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure_queries&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;ss&quot;&gt;:queries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SELECT &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.* FROM &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; LIMIT $1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:total_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:min_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:max_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:avg_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
   &lt;span class=&quot;s2&quot;&gt;&quot;SELECT &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;teams&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.* FROM &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;teams&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; WHERE &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;teams&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; = $1 LIMIT $2&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:total_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.94&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:min_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.62&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:max_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.37&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
     &lt;span class=&quot;ss&quot;&gt;:avg_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.94&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}},&lt;/span&gt;
 &lt;span class=&quot;ss&quot;&gt;:total_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;13.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;ss&quot;&gt;:sql_duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;11.34&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In theory, you can extract the same data by piping ActiveRecord logging to &lt;em&gt;STDOUT&lt;/em&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;logger&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STDOUT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Produces:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;D, [2023-03-01T13:57:42.815016 #19045] DEBUG -- :   User Load (1.6ms)  SELECT &quot;users&quot;.* FROM &quot;users&quot; ORDER BY &quot;users&quot;.&quot;id&quot; DESC LIMIT $1  [[&quot;LIMIT&quot;, 20]]
D, [2023-03-01T13:57:42.817497 #19045] DEBUG -- :   Team Load (2.1ms)  SELECT &quot;teams&quot;.* FROM &quot;teams&quot; WHERE &quot;teams&quot;.&quot;id&quot; = $1 LIMIT $2  [[&quot;id&quot;, &quot;[FILTERED]&quot;], [&quot;LIMIT&quot;, 1]]
D, [2023-03-01T13:57:42.819068 #19045] DEBUG -- :   Team Load (1.2ms)  SELECT &quot;teams&quot;.* FROM &quot;teams&quot; WHERE &quot;teams&quot;.&quot;id&quot; = $1 LIMIT $2  [[&quot;id&quot;, &quot;[FILTERED]&quot;], [&quot;LIMIT&quot;, 1]]
(.....N+1 more lines.....)
D, [2023-03-01T13:57:42.843380 #19045] DEBUG -- :   Team Load (1.0ms)  SELECT &quot;teams&quot;.* FROM &quot;teams&quot; WHERE &quot;teams&quot;.&quot;id&quot; = $1 LIMIT $2  [[&quot;id&quot;, &quot;[FILTERED]&quot;], [&quot;LIMIT&quot;, 1]]
D, [2023-03-01T13:57:42.844977 #19045] DEBUG -- :   Team Load (1.4ms)  SELECT &quot;teams&quot;.* FROM &quot;teams&quot; WHERE &quot;teams&quot;.&quot;id&quot; = $1 LIMIT $2  [[&quot;id&quot;, &quot;[FILTERED]&quot;], [&quot;LIMIT&quot;, 1]]&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But it’s arguably less readable. And for more complex cases, it is likely to produce an endless log stream instead of an easy-to-comprehend hash aggregating all the necessary data.&lt;/p&gt;

&lt;p&gt;Optionally, with the help of &lt;a href=&quot;https://github.com/basecamp/marginalia&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;marginalia gem&lt;/a&gt;, you can add backtraces info to standard ActiveRecord logs output. As an effect it will be included in the measured queries data:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Marginalia gem appends origin line numbers to query logs&quot; title=&quot;Marginalia gem appends origin line numbers to query logs&quot; loading=&quot;lazy&quot; src=&quot;/assets/marginalia-logs-ec52d049a5ce664f5d11e892e7eb02d3352f08ce44dcccb10cc79e2a5330cf35.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;Marginalia gem appends origin line numbers to measured query logs&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;You have to configure Marginalia to enable this feature:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/development.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Marginalia&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;components&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I’m using the &lt;code class=&quot;highlighter-rouge&quot;&gt;measure_queries&lt;/code&gt; helper method for my ongoing Rails performance audits. It has significantly improved my workflow allowing for faster iteration on N+1 related fixes. I no longer have to use a rack-mini-profiler UI. Instead, I can now apply the change, type &lt;code class=&quot;highlighter-rouge&quot;&gt;reload!&lt;/code&gt; (&lt;a href=&quot;/rails-debug-aliases&quot;&gt;aliased to &lt;code class=&quot;highlighter-rouge&quot;&gt;re&lt;/code&gt;&lt;/a&gt;) straight in the Rails console and quickly measure if a refactored method is faster and triggers fewer queries. Being able to debug on the lower layer of models/services instead of HTTP endpoints is a huge productivity boost.&lt;/p&gt;

&lt;p&gt;Since this API is relatively new I’m open to feedback on improving it.&lt;/p&gt;

&lt;h2 id=&quot;use-specs-to-prevent-n1-queries&quot;&gt;Use specs to prevent N+1 queries&lt;/h2&gt;

&lt;p&gt;Another way to keep the N+1 problem under control is to leverage your test suite. You can use the same &lt;code class=&quot;highlighter-rouge&quot;&gt;measure_queries&lt;/code&gt; to constraint how many queries any Ruby code snippet is &lt;em&gt;allowed&lt;/em&gt; to trigger. Arguably the simplest way to use it is on a per-endpoint level with controller specs:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UsersController&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;index&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;does not exceed queries limit&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;queries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;measure_queries&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:index&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;queries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;be&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The above spec will fail if the number of queries exceeds a predefined threshold. Alternatively, you can check out the &lt;a href=&quot;https://github.com/palkan/n_plus_one_control&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;n_plus_one_control gem&lt;/a&gt; for more complex rspec matchers related to N+1 queries.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;With adequate tooling, it’s possible to spot and resolve N+1 issues before they reach production. I hope that some of the above tips will help you improve your workflow when fixing these problems.&lt;/p&gt;
</description>
        <pubDate>Tue, 07 Mar 2023 04:09:06 +0100</pubDate>
        <link>https://pawelurbanek.com/rails-debug-queries</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-debug-queries</guid>
      </item>
    
      <item>
        <title>How to Subscribe to YouTube Channels Without an Account</title>
        <description>&lt;p&gt;I’m extremely cautious with YouTube. I’ve lost countless hours because of their flawless recommendation algorithms. In one of my previous posts, I describe &lt;a href=&quot;/youtube-addiction-selfcontrol&quot;&gt;how to &lt;em&gt;“cripple”&lt;/em&gt; YouTube recommendations UI&lt;/a&gt; and start using it more deliberately. I also prefer to use YouTube without logging in, but it makes following my favorite channels difficult. So in this tutorial, I describe how to over-engineer your way to subscribing to YouTube channels without having an account.&lt;/p&gt;

&lt;h2 id=&quot;how-to-find-an-rss-feed-of-a-youtube-channel&quot;&gt;How to find an RSS feed of a YouTube channel?&lt;/h2&gt;

&lt;p&gt;Every YouTube channel has an &lt;em&gt;undocumented&lt;/em&gt; RSS feed representing its content. But it’s not available in the UI. Instead, you have to dig into the HTML source code to extract it:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;RSS feed in YouTube the web page source&quot; title=&quot;RSS feed in YouTube the web page source&quot; loading=&quot;lazy&quot; src=&quot;/assets/source-rss-feed-73b05662f2923e75ae8d05eb97bf8196021d7f9473cec986edab0793c3a7d6ad.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;In the channel &quot;Videos&quot; web page source code, search for &lt;code&gt;RSS&lt;/code&gt; to find the DOM element with the feed address&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Or, even simpler, you can run this JS code in the console to extract an RSS address:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'[title=&quot;RSS&quot;]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;href&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;YouTube RSS feed extracted via JS&quot; title=&quot;YouTube RSS feed extracted via JS&quot; loading=&quot;lazy&quot; src=&quot;/assets/youtube-rss-feed-01ba09d97438eb40aade4668da61b82e77f943cf379d9aebb4a90d43aa3be0f4.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;Remember to search &quot;Videos&quot; and not &quot;Home&quot; tab of a channel for the RSS feed&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Once you have RSS feeds of your favorite channels, you can use any RSS app to monitor them. I like to use Slack for this purpose. An official &lt;a href=&quot;https://slack.com/help/articles/218688467-Add-RSS-feeds-to-Slack&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Slack RSS app&lt;/a&gt; sends a channel message when there’s a new RSS item.&lt;/p&gt;

&lt;p&gt;Alternatively, if you’re on a paid Zapier plan, you can batch your RSS notifications with their &lt;a href=&quot;https://zapier.com/blog/zapier-delay-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Delay Zap Action&lt;/a&gt; to arrive at a specific time. Or, if you prefer self-hosted solutions, you can check out a &lt;a href=&quot;https://github.com/huginn/huginn&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Huginn Ruby gem&lt;/a&gt; or &lt;a href=&quot;https://www.activepieces.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ActivePieces&lt;/a&gt; for similar functionality.&lt;/p&gt;

&lt;p&gt;YouTube channels are just an example. You can use this technique for any source exposed via an RSS feed. For me, RSS is a superior way to consume social media. No ads, no click baits, just hand-picked content delivered straight to your Slack channel. At a time of your and not &lt;em&gt;user engagement algorithm&lt;/em&gt; choice.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Zapier RSS notification in Slack channel&quot; title=&quot;Zapier RSS notification in Slack channel&quot; loading=&quot;lazy&quot; src=&quot;/assets/zapier-rss-notification-04cc743e59176178952e8fc892215e8e809e12b731e72c237f6d3ce7f6d47b0b.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;Custom Zapier notifications in Slack&lt;/div&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Consuming most social media via RSS feeds allowed me to reduce my addiction to garbage content. Or at least I can now deliberately choose garbage I like without going on a binge. Most social media platforms prioritize time spent scrolling instead of quality. So similar overengineered tricks could be a way to break their &lt;em&gt;“optimizations”&lt;/em&gt; and defend your time and focus.&lt;/p&gt;
</description>
        <pubDate>Thu, 16 Feb 2023 08:09:06 +0100</pubDate>
        <link>https://pawelurbanek.com/youtube-without-account</link>
        <guid isPermaLink="true">https://pawelurbanek.com/youtube-without-account</guid>
      </item>
    
      <item>
        <title>Easy to Overlook Way to Break Eager Loading in Rails Apps</title>
        <description>&lt;p&gt;In theory, configuring eager loading to avoid N+1 issues is straightforward. Chaining &lt;code class=&quot;highlighter-rouge&quot;&gt;includes&lt;/code&gt; method is usually enough to ensure that all the relations data is fetched efficiently. However, this rule breaks for some easy-to-overlook edge cases. In this blog post, I’ll describe how to eager load ActiveRecord relations using custom SQL ORDER BY sorting.&lt;/p&gt;

&lt;h2 id=&quot;how-to-use-eager-loading-with-order-by-in-sql&quot;&gt;How to use eager loading with ORDER BY in SQL?&lt;/h2&gt;

&lt;p&gt;Based on my experience conducting Rails performance audits, N+1 issues are a primary cause of sub-optimal response times in Ruby on Rails apps. ActiveRecord makes it just too easy to sneak in an N+1 bug in production. It’s easy to miss it in development because you usually work with small datasets.&lt;/p&gt;

&lt;p&gt;Let’s analyze a sample scenario of displaying the products of a user:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:products&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/models/product.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:by_rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;rating: :desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/controllers/users_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UsersController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In theory, this implementation should correctly eager-load all the product objects and prevent N+1 queries.&lt;/p&gt;

&lt;p&gt;But let’s consider the following view layer implementation:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/views/users/index.html.erb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt; products &lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;by_rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The addition of &lt;code class=&quot;highlighter-rouge&quot;&gt;by_rating&lt;/code&gt; scope effectively breaks the eager loading. Adding a &lt;code class=&quot;highlighter-rouge&quot;&gt;where&lt;/code&gt; call would have the same effect. It is caused by chaining methods that alter the relation SQL and invalidate the previously eager loaded collection. It’s even worse than not using eager loading at all. &lt;code class=&quot;highlighter-rouge&quot;&gt;includes&lt;/code&gt; still triggers additional queries, but results are discarded.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;naive&lt;/em&gt; approach to avoiding N+1 issues in similar situations could be to work with methods that do not alter the relation SQL. It means that instead of adding &lt;code class=&quot;highlighter-rouge&quot;&gt;by_rating&lt;/code&gt; scope, you’d need to use a method that does the sorting in Ruby:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/views/products/index.html.erb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort_by&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;rating&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Similarly, instead of &lt;code class=&quot;highlighter-rouge&quot;&gt;where&lt;/code&gt;, you could use the &lt;code class=&quot;highlighter-rouge&quot;&gt;select&lt;/code&gt; method. It filters collection in Ruby instead of an SQL &lt;code class=&quot;highlighter-rouge&quot;&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;h2 id=&quot;using-custom-relation-to-prevent-n1-queries&quot;&gt;Using custom relation to prevent N+1 queries&lt;/h2&gt;

&lt;p&gt;However, there are downsides to using Ruby instead of SQL to transform relation objects. For example, sorting in Ruby can be slower than SQL &lt;code class=&quot;highlighter-rouge&quot;&gt;ORDER BY&lt;/code&gt; or even impossible for larger data sizes. And using &lt;code class=&quot;highlighter-rouge&quot;&gt;select&lt;/code&gt; to filter the collection means that you’ll discard already initialized ActiveRecord objects, so memory is wasted.&lt;/p&gt;

&lt;p&gt;A better way to work with relations that need their SQL customized is to use a dedicated relation method. Let’s see it in action:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:products&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:products_by_rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;by_rating&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class_name: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Product'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/controllers/users_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UsersController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:products_by_rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/views/products/index.html.erb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;products_by_rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This approach eliminates the additional N+1 queries while preserving custom order without a need for sorting in Ruby.&lt;/p&gt;

&lt;p&gt;For non-trivial views, it could be challenging to prevent N+1 issues from emerging accidentally. For example, even custom relations could still trigger N+1 queries if their SQL is further customized. One way to avoid it could be to explicitly call &lt;code class=&quot;highlighter-rouge&quot;&gt;to_a&lt;/code&gt; on relations collections before using them. As an effect, &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord::Relation&lt;/code&gt; is transformed into an &lt;code class=&quot;highlighter-rouge&quot;&gt;Array&lt;/code&gt; of objects, making it impossible to chain SQL calls.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;by_rating&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# raises =&amp;gt; undefined method `by_rating' for Array (NoMethodError)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But it’s arguably not the cleanest solution.&lt;/p&gt;

&lt;p&gt;Unfortunatelly, even using the recently added &lt;code class=&quot;highlighter-rouge&quot;&gt;strict_loading&lt;/code&gt; on a relation would not detect similar mistakes:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;strict_loading: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# raises =&amp;gt; ActiveRecord::StrictLoadingViolationError&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:by_rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# loads the Products but triggers N+1 queries&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;You should continuously monitor the number of queries executed by your application. I always recommend using &lt;a href=&quot;https://github.com/MiniProfiler/rack-mini-profiler&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rack-mini-profiler&lt;/a&gt; and regularly inspecting SQL queries triggered by each view. This &lt;a href=&quot;http://railscasts.com/episodes/368-miniprofiler&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;10-year-old Railscast&lt;/a&gt; is still up to date, so the tool did stand the test of time. Getting rid of sneaky N+1 issues could be the lowest-hanging fruit in optimizing your Rails app’s performance.&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Jan 2023 06:09:06 +0100</pubDate>
        <link>https://pawelurbanek.com/rails-eager-ordered</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-eager-ordered</guid>
      </item>
    
      <item>
        <title>How to Monitor and Fix PostgreSQL Database Locks in Rails</title>
        <description>&lt;p&gt;PostgreSQL database locks usually work seamlessly until they don’t. Before your Rails app dataset and traffic reach a certain scale, you’re unlikely to face any locks-related issues. But if your app suddenly slows down to a crawl, deadlocks likely are to blame. In this blog post, I’ll describe how to monitor database locks and propose best practices to prevent them from causing issues.&lt;/p&gt;

&lt;h2 id=&quot;postgresql-database-locks-101&quot;&gt;PostgreSQL database locks 101&lt;/h2&gt;

&lt;p&gt;Locks in PostgreSQL are a mechanism for ensuring data consistency in multithreaded environments. This blog post is not trying to be a comprehensive introduction to how locks and related MVVC (Multi-Version Concurrency Control) system works in PG databases. Instead, I’ll focus on the basics to help us understand how locks affect Ruby on Rails apps’ performance and stability.&lt;/p&gt;

&lt;p&gt;It’s important to understand that every single write to the database acquires a lock. &lt;em&gt;Well-behaved&lt;/em&gt; databases usually commit changes in a few &lt;em&gt;ms&lt;/em&gt;, so observing related locks is not straightforward. As a workaround, you can simulate a long-lasting lock by running a similar code in the Rails console:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transaction&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SecureRandom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;25&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;center annotation&quot;&gt;The new column value has to differ from the current one for locks to be acquired. &lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Now in a separate Rails console process run the &lt;code class=&quot;highlighter-rouge&quot;&gt;locks&lt;/code&gt; method from &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras gem&lt;/a&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;locks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;in_format: :hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# [&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;pid&quot;: 64,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;relname&quot;: &quot;users_pkey&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;transactionid&quot;: null,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;granted&quot;: true,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;mode&quot;: &quot;RowExclusiveLock&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;query_snippet&quot;: &quot;UPDATE \&quot;users\&quot; SET \&quot;email\&quot; = $1 WHERE \&quot;users\&quot;.\&quot;id\&quot; = $2&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;age&quot;: &quot;PT0.954817S&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;application&quot;: &quot;bin/rails&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   },&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;pid&quot;: 64,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;relname&quot;: &quot;users&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;transactionid&quot;: null,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;granted&quot;: true,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;mode&quot;: &quot;RowExclusiveLock&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;query_snippet&quot;: &quot;UPDATE \&quot;users\&quot; SET \&quot;email\&quot; = $1 WHERE \&quot;users\&quot;.\&quot;id\&quot; = $2&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;age&quot;: &quot;PT0.954817S&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;application&quot;: &quot;bin/rails&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   },&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;pid&quot;: 64,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;relname&quot;: &quot;index_users_on_email&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;transactionid&quot;: null,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;granted&quot;: true,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;mode&quot;: &quot;RowExclusiveLock&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;query_snippet&quot;: &quot;UPDATE \&quot;users\&quot; SET \&quot;email\&quot; = $1 WHERE \&quot;users\&quot;.\&quot;id\&quot; = $2&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;age&quot;: &quot;PT0.954817S&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;application&quot;: &quot;bin/rails&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   },&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;pid&quot;: 64,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;relname&quot;: &quot;index_users_on_api_token&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;transactionid&quot;: null,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;granted&quot;: true,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;mode&quot;: &quot;RowExclusiveLock&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;query_snippet&quot;: &quot;UPDATE \&quot;users\&quot; SET \&quot;email\&quot; = $1 WHERE \&quot;users\&quot;.\&quot;id\&quot; = $2&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;age&quot;: &quot;PT0.954817S&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;application&quot;: &quot;bin/rails&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   },&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# ]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, even a straightforward single-column update operation acquires an array of &lt;code class=&quot;highlighter-rouge&quot;&gt;RowExclusiveLock&lt;/code&gt; locks on a corresponding table and all the indexes. Usually, this process works seamlessly behind the scenes. A lock affecting a single row is not critical (apart from slowing down a single request/job) as long as it’s not blocking other queries.&lt;/p&gt;

&lt;p&gt;But let’s see what happens when we try to update the same row concurrently.&lt;/p&gt;

&lt;p&gt;In the first Rails console process run:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transaction&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;25&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Then in the second one:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SecureRandom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;and now, in the third Rails console process, we can investigate the collision of concurrent update operations:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blocking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;in_format: :hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# [&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   {&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;blocked_pid&quot;: 64,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;blocking_statement&quot;: &quot;UPDATE \&quot;users\&quot; SET \&quot;email\&quot; = $1 WHERE \&quot;users\&quot;.\&quot;id\&quot; = $2&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;blocking_duration&quot;: &quot;PT20.450116S&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;blocking_pid&quot;: 152,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;blocked_statement&quot;: &quot;UPDATE \&quot;users\&quot; SET \&quot;email\&quot; = $1 WHERE \&quot;users\&quot;.\&quot;id\&quot; = $2&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#     &quot;blocked_duration&quot;: &quot;PT8.145238S&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   }&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# ]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, the acquired lock prevents the second SQL statement from finishing. It’s reported by &lt;code class=&quot;highlighter-rouge&quot;&gt;blocking&lt;/code&gt; method from rails-pg-extras. In practice, a web server request trying to execute this SQL statement would be stuck until the first lock is released. This is how &lt;em&gt;misbehaving&lt;/em&gt; locks can kill the performance of your application or even cause downtime by exhausting available web server connections.&lt;/p&gt;

&lt;h2 id=&quot;monitoring-postgresql-locks-in-production&quot;&gt;Monitoring PostgreSQL locks in production&lt;/h2&gt;

&lt;p&gt;You’re unlikely to sprinkle &lt;code class=&quot;highlighter-rouge&quot;&gt;sleep&lt;/code&gt; all over your codebase to simplify investigating database locks. Here’s a sample implementation of a Sidekiq job that can help you monitor long-lasting locks and blocked SQL statements in production:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/initializers/pg_monitoring.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;LocksMonitoringJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PG_MONITOR_LOCKS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/jobs/locks_monitoring_job.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LocksMonitoringJob&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Sidekiq&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Worker&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;sidekiq_options&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;retry: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

  &lt;span class=&quot;no&quot;&gt;PG_MIN_LOCK_DURATION_MS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PG_MIN_LOCK_DURATION_MS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_f&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;PG_MONITORING_INTERVAL_SECONDS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PG_MONITORING_INTERVAL_SECONDS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;SLACK_WEBHOOK_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SLACK_WEBHOOK_URL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;locks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;locks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;in_format: :hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PG_MIN_LOCK_DURATION_MS&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;locks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Notifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;no&quot;&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;channel: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pg-alerts&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ping&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;locks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;blocking&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blocking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;in_format: :hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActiveSupport&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;blocking_duration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PG_MIN_LOCK_DURATION_MS&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;blocking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Notifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;no&quot;&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;channel: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pg-alerts&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ping&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;blocking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;ensure&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;LocksMonitoringJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform_in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PG_MONITORING_INTERVAL_SECONDS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PG_MONITOR_LOCKS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;true&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;We use an initializer file to schedule the first task and can optionally disable it by removing &lt;code class=&quot;highlighter-rouge&quot;&gt;PG_MONITOR_LOCKS&lt;/code&gt; variable. The job schedules itself every configurable &lt;code class=&quot;highlighter-rouge&quot;&gt;PG_MONITORING_INTERVAL_SECONDS&lt;/code&gt; and reports locks and blocked queries that cross a &lt;code class=&quot;highlighter-rouge&quot;&gt;PG_MIN_LOCK_DURATION_MS&lt;/code&gt; threshold using &lt;a href=&quot;https://github.com/slack-notifier/slack-notifier&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;slack-notifier gem&lt;/a&gt; (check out &lt;a href=&quot;https://api.slack.com/messaging/webhooks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Slack docs&lt;/a&gt; for info on how to configure incoming webhooks). You can tweak the ENV variables to adjust monitoring sensitivity based on your app’s requirements.&lt;/p&gt;

&lt;p&gt;If your app is running heavyweight background jobs, you’ll likely have to manually exclude some queries from being raported. And if your current database performance is far-from-perfect, you should bump up the &lt;code class=&quot;highlighter-rouge&quot;&gt;PG_MIN_LOCK_DURATION_MS&lt;/code&gt; and gradually lower it when you manage fix the cause of long-lasting locks. Excluding background jobs and data migrations, a healthy database should acquire exclusive locks for a maximum of &lt;em&gt;~100-200ms&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Running a similar monitoring task can help you spot many lock-related issues early on before they start affecting end users.&lt;/p&gt;

&lt;h2 id=&quot;how-to-resolve-lock-related-issues-in-rails-apps&quot;&gt;How to resolve lock-related issues in Rails apps&lt;/h2&gt;

&lt;p&gt;So your Slack alerts  channel is spammed with long-lasting locks and blocked query notifications. What now?&lt;/p&gt;

&lt;h3 id=&quot;overusing-activerecordbasetransaction-scope&quot;&gt;Overusing ActiveRecord::Base.transaction scope&lt;/h3&gt;

&lt;p&gt;I’ve seen &lt;em&gt;misbehaving&lt;/em&gt; locks with a duration of a few seconds. Surprsingly, the underlying cause wasn’t even directly caused by the database layer. ActiveRecord makes it too easy to wrap any arbitrary chunk of code into a database transaction. Have a look at the following example:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;after_create&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:send_welcome_email&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;send_email&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;EmailSender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send_welcome_email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;ActiveRecord callbacks are a usual way to trigger side effects based on the model object’s lifecycle. For example, sending a welcome email to newly registered users is a common use case. Let’s keep discussing whether similar logic belongs in a model aside for now. Instead, let’s focus on how the database layer will react to a similar implementation.&lt;/p&gt;

&lt;p&gt;Something to watch out for is that excluding &lt;code class=&quot;highlighter-rouge&quot;&gt;after_commit&lt;/code&gt;, all the AR callbacks are implicitly wrapped in a database transaction. So, the above implementation would keep a &lt;code class=&quot;highlighter-rouge&quot;&gt;RowExclusiveLock&lt;/code&gt; on the user object for the whole time needed to connect to 3rd party API and send an email. So, instead of the expected max &lt;em&gt;~200ms&lt;/em&gt; threshold, our lock could last up to 60 seconds in case there’s an API timeout.&lt;/p&gt;

&lt;p&gt;This example might seem contrived. But I’ve seen multiple Rails apps suffering from long-lasting locks caused by implicit 3rd party HTTP calls executed inside database transactions.&lt;/p&gt;

&lt;p&gt;The hidden complexity of Rails ActiveRecord makes it too easy to commit similar mistakes, even for more experienced devs. That’s why proper locks monitoring can help you spot similar issues, even if they go unnoticed during the code review.&lt;/p&gt;

&lt;h3 id=&quot;other-ways-to-fix-long-lasting-locks&quot;&gt;Other ways to fix long-lasting locks&lt;/h3&gt;

&lt;p&gt;Apart from limiting the scope of ActiveRecord transactions, the &lt;em&gt;“simplest”&lt;/em&gt; way to decrease locks duration is to optimize the performance of the database layer. Check out &lt;a href=&quot;/postgresql-fix-performance&quot;&gt;this blog post&lt;/a&gt; for an in-depth guide on analyzing and fixing PostgreSQL performance with the help of &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras gem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But, fine-tuning SQL queries is often a more time-involved process. So, here’s a list of &lt;em&gt;“quick &amp;amp; easy”&lt;/em&gt; ad hoc solutions to try before you manage to deploy proper fixes. These are &lt;em&gt;“hacks”&lt;/em&gt;, but they might help if your app is currently unstable because of locks-related issues.&lt;/p&gt;

&lt;h3 id=&quot;1-throttle-sidekiq-jobs&quot;&gt;1. Throttle Sidekiq jobs&lt;/h3&gt;

&lt;p&gt;One way to limit the occurances of locks is to reduce the concurrency of database commits. It will limit the scalability and throughput of your app. But the tradeoff might be worth it if you’re experiencing locks-related downtimes.&lt;/p&gt;

&lt;p&gt;I usually recommend implementing the Sidekiq config file in a similar way:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/sidekiq.yml&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;s&quot;&gt;:concurrency&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV.fetch(&quot;SIDEKIQ_CONCURRENCY&quot;) { 5 } %&amp;gt;&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;:queues&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;urgent&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;default&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Even if you’re currently not planning to tweak the concurrency setting, using an ENV variable will allow you to easily change this value without doing a code-change release. A &lt;em&gt;panic fix&lt;/em&gt; could be to throttle concurrency down to &lt;code class=&quot;highlighter-rouge&quot;&gt;1&lt;/code&gt; to minimize concurrent locks’ impact before you apply the proper solution.&lt;/p&gt;

&lt;p&gt;Alternatively, if you know which Sidekiq job is causing the issue, you could throttle it from running concurrently with the help of the &lt;a href=&quot;https://github.com/mhenrixon/sidekiq-unique-jobs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;sidekiq-unique-jobs gem&lt;/a&gt;. This is also a good solution for more heavyweight jobs to limit the max memory usage of Sidekiq.&lt;/p&gt;

&lt;h3 id=&quot;2-limit-the-apps-concurrency&quot;&gt;2. Limit the app’s concurrency&lt;/h3&gt;

&lt;p&gt;Similar throttling can be done for web app processes. Limiting your app’s concurrency could reduce performance by increasing request queue time. But, it’s possible that the &lt;em&gt;throttled&lt;/em&gt; app will be more performant because blocking SQL queries are limited.&lt;/p&gt;

&lt;p&gt;You can implement your Puma web server config in a similar way to easily tweak settings without a need for code release:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/puma.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;threads_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RAILS_MAX_THREADS&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;threads&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;threads_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;threads_count&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;workers&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;WEB_CONCURRENCY&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;3-kill-stuck-connections&quot;&gt;3. Kill stuck connections&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;RailsPgExtras.locks&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;RailsPgExtras.blocking&lt;/code&gt; could report that a single long-running query is blocking dozens of other database commits. Depending on the scale of the issue, a similar &lt;em&gt;traffic jam&lt;/em&gt; could cause many of your web server connections to get stuck and your app to become unresponsive. A &lt;em&gt;“panic button”&lt;/em&gt; solution for this issue that could potentially restore your app’s responsiveness is to kill the offending connection. rails-pg-extras &lt;code class=&quot;highlighter-rouge&quot;&gt;blocking&lt;/code&gt; method provides a &lt;code class=&quot;highlighter-rouge&quot;&gt;blocking_pid&lt;/code&gt; value you can use to cherry-pick and terminate the stuck SQL query. Additionally, you can use &lt;code class=&quot;highlighter-rouge&quot;&gt;RailsPgExtras.connections&lt;/code&gt; to verify a process name that generated the problematic query. It could be helpful for debugging.&lt;/p&gt;

&lt;p&gt;You can terminate an SQL query/connection based on its PID value by running the following code:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;kill_pid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;args: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pid: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4657&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Optionally, if your database is a bad enough shape that killing a single connection is not enough, you can run:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;RailsPgExtras&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;kill_all&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Remember to restart all the web and background worker processes to prevent &lt;em&gt;ghost&lt;/em&gt; connections from hanging around.&lt;/p&gt;

&lt;p&gt;As I’ve mentioned, these are not proper fixes, but rather &lt;em&gt;“band-aids”&lt;/em&gt; that might help in case your app is unstable.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;PostgreSQL database locks usually work seamlessly behind the scenes. But, following the mentioned best practices and configuring correct monitoring can save you a lot of headaches. A proactive approach to monitoring lock-related issues can help you resolve most of the problems before they start affecting your users.&lt;/p&gt;

</description>
        <pubDate>Tue, 20 Dec 2022 09:09:06 +0100</pubDate>
        <link>https://pawelurbanek.com/rails-postgresql-locks</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-postgresql-locks</guid>
      </item>
    
      <item>
        <title>Easy to Miss Way to Optimize ActiveRecord SQL Memory Usage in Rails</title>
        <description>&lt;p&gt;By default, Rails ActiveRecord executes most of the SQL queries in a non-optimal way. In this blog post, I’ll describe how to fix this issue to speed up bottlenecks and reduce memory usage.&lt;/p&gt;

&lt;h2 id=&quot;bloated-activerecord-queries&quot;&gt;Bloated ActiveRecord queries&lt;/h2&gt;

&lt;p&gt;Active Record empowers developers to write fairly complex database queries without understanding the underlying SQL. Unfortunately, the same ActiveRecord is a reason for the majority of the issues that I encounter while conducting my Rails performance audits. Next to the &lt;em&gt;infamous&lt;/em&gt; N+1 calls, &lt;em&gt;“bloated”&lt;/em&gt; queries are another common problem.&lt;/p&gt;

&lt;p&gt;Let’s see it in action:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# == Schema Information&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Table name: users&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  id                     :bigint           not null, primary key&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  api_auth_token         :string           not null&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  block_uuid             :uuid&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  blocked_from           :datetime&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  blocked_until          :datetime&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  messaging_active_today :boolean          default(FALSE), not null&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  pseudonym              :string&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  uuid                   :uuid             not null&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  voting_active_today    :boolean          default(FALSE), not null&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  web_auth_token         :string&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#  team_id                :integer          not null&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;blocked?&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;blocked_until&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;blocked_until&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;annotation center&quot;&gt;Model schema annotations can be added using the &lt;a class=&quot;link link-grey&quot; href=&quot;https://github.com/ctran/annotate_models&quot; target=&quot;_blank&quot;&gt;annotate_models gem&lt;/a&gt;. I highly recommend it!&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/controllers/users_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UsersController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:updated_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;app/views/users/index.html.erb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      ID: &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      Blocked: &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blocked?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;It is common for core models in legacy Rails projects to consist of dozens of columns. In the above example, we fetch 200 rows from the database and display info based on the logic implemented in a &lt;code class=&quot;highlighter-rouge&quot;&gt;User&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;SQL query executed in the controlled looks like that:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;SELECT &quot;users&quot;.* FROM &quot;users&quot; ORDER BY &quot;updated_at&quot; ASC LIMIT 200&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Can you spot the issue now? A little hint: &lt;code class=&quot;highlighter-rouge&quot;&gt;*&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;By default, ActiveRecord fetches &lt;em&gt;ALL&lt;/em&gt; the model columns. But, in the above example, we only really need the &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;blocked_until&lt;/code&gt; columns for our view to display correctly. If you only care about raw data, a recommended practice is using the ActiveRecord &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; method to avoid the overhead of instantiating model objects.&lt;/p&gt;

&lt;p&gt;But, in this case, we need the model logic. So to improve performance, we can use a &lt;code class=&quot;highlighter-rouge&quot;&gt;select&lt;/code&gt; method:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UsersController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:updated_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:blocked_until&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The above implementation would produce the following SQL query:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;SELECT &quot;users&quot;.&quot;id&quot;, &quot;users&quot;.&quot;blocked_until&quot; FROM &quot;users&quot; ORDER BY &quot;updated_at&quot; ASC LIMIT 200&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Contrary to &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt;, chaining a &lt;code class=&quot;highlighter-rouge&quot;&gt;select&lt;/code&gt; method would still return ActiveRecord object instances, so you’ll be able to call methods implemented in the model. One catch is that if you try to read a value from a column that was not fetched from the database, an &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveModel::MissingAttributeError&lt;/code&gt; would be raised.&lt;/p&gt;

&lt;h2 id=&quot;how-to-reduce-activerecord-ram-usage&quot;&gt;How to reduce ActiveRecord RAM usage&lt;/h2&gt;

&lt;p&gt;Now that we’ve discussed how to slim down &lt;em&gt;bloated&lt;/em&gt; queries, let’s measure the potential impact on memory usage. &lt;a href=&quot;https://github.com/SamSaffron/memory_profiler&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;memory_profiler gem&lt;/a&gt; offers a simple API for measuring in-memory object allocations.&lt;/p&gt;

&lt;p&gt;Let’s start with checking how many objects are allocated when we fetch 1000 instances of a sample model with 12 columns:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;MemoryProfiler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pretty_print&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;annotation center&quot;&gt;We append &lt;code&gt;0;&lt;/code&gt; to prevent IRB from printing the query output to the console and skew memory usage results.&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;MemoryProfiler&lt;/code&gt; outputs loads of data, but we’re interested in the top few lines:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Total allocated: 1723042 bytes (11577 objects)
Total retained:  1536160 bytes (10183 objects)

allocated memory by gem
-----------------------------------
   1522398  activerecord-7.0.4
    176080  activemodel-7.0.4
    ...&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Total allocated: 1723042 bytes&lt;/code&gt; represents memory usage increase caused by running the code snippet. The absolute value is not important, but it will serve as a reference point when we compare more efficient ways to run this query. The absolute memory usage would vary based on total columns number and the model implementation.&lt;/p&gt;

&lt;p&gt;Let’s now see how memory usage looks if we &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; two columns:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;MemoryProfiler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:blocked_until&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pretty_print&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Total allocated: 204350 bytes (4422 objects)
Total retained:  3252 bytes (48 objects)

allocated memory by gem
-----------------------------------
    141347  activerecord-7.0.4
     40040  local/lib
     ...&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That’s a &lt;strong&gt;~90% decrease&lt;/strong&gt;! But, as previously mentioned, &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; could be limiting in some cases since it returns raw data instead of model objects. So let’s now measure the memory impact of using &lt;code class=&quot;highlighter-rouge&quot;&gt;select&lt;/code&gt; to fetch only two columns:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;MemoryProfiler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:blocked_until&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pretty_print&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Total allocated: 613271 bytes (7432 objects)
Total retained:  531648 bytes (6055 objects)

allocated memory by gem
-----------------------------------
    412930  activerecord-7.0.4
    176080  activemodel-7.0.4
    ...&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That’s ~65% less than the original &lt;code class=&quot;highlighter-rouge&quot;&gt;*&lt;/code&gt; query. And you can still interact with full model APIs as long as non-fetched column values are not accessed.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Chart showing memory usage depending of SQL query format&quot; title=&quot;Chart showing memory usage depending of SQL query format&quot; loading=&quot;lazy&quot; src=&quot;/assets/sql-memory-usage-e99fa4c00f4deab8605e7785667da43c891fdb35312df33487a86ccb9d142c17.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;annotation center&quot;&gt;Memory usage comparison depending on SQL query format&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Another advantage of putting &lt;em&gt;bloated&lt;/em&gt; SQL queries on a diet is that fetching less columns will decrease the number of disk-read operations. Database read IOPS are sometimes a bottleneck for apps with larger traffic. Fetching fewer columns is a relatively straightforward way to improve this metric.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Like with most SQL-related issues, the benefits of fine-tuning queries will only provide measurable effect once the app and dataset reach a certain scale. But, I’ve seen significant performance and memory usage improvements from limiting columns fetched from the database. You can check out &lt;a href=&quot;/postgresql-fix-performance&quot;&gt;my blog post about rails-pg-extras&lt;/a&gt; to learn how to find queries that are likely to benefit from this optimization technique.&lt;/p&gt;
</description>
        <pubDate>Mon, 05 Dec 2022 07:39:06 +0100</pubDate>
        <link>https://pawelurbanek.com/activerecord-memory-usage</link>
        <guid isPermaLink="true">https://pawelurbanek.com/activerecord-memory-usage</guid>
      </item>
    
      <item>
        <title>Rails Quick Tip - Use Private Debugging Aliases</title>
        <description>&lt;p&gt;I don’t like to type much. Even minor improvements in your debugging workflow are likely to accumulate into huge keystrokes savings over time. In this blog post, I’ll describe a simple way to add debugging shortcuts to the project without modifying the codebase shared with other team members.&lt;/p&gt;

&lt;h2 id=&quot;my-aliases-good-your-aliases-bad&quot;&gt;My aliases good, your aliases bad&lt;/h2&gt;

&lt;p&gt;In one of my previous posts, I described a way to &lt;a href=&quot;/rails-console-aliases&quot;&gt;improve your productivity by using Rails console aliases&lt;/a&gt;. The downside of the described approach is that the &lt;code class=&quot;highlighter-rouge&quot;&gt;.irbrc&lt;/code&gt; file is usually committed to the repository.&lt;/p&gt;

&lt;p&gt;If you’re working in a larger team, it might be challenging to agree on a definitive list of aliases that everyone finds useful. So instead of starting code review battles on which aliases are worth committing to the shared repo, you can keep your private aliases collection.&lt;/p&gt;

&lt;p&gt;In theory, you could leverage an &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.irbrc&lt;/code&gt; file to define them. But, the problem is that if you start an IRB session outside of the context of a Rails project, any of the calls to the custom classes or included gems would break. Also, it does not seem correct to include per-project customizations in global dotfiles.&lt;/p&gt;

&lt;p&gt;Instead, you can customize a project without modifying its shared source code. You’ll have to configure git to parse the file with a list of entries that should never be committed to any of the local repositories:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;~/.gitconfig&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;core]
  excludesfile &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; ~/.gitignore_global&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You can now define any file to be excluded from git commits:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;~/.gitignore_global&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;config/initializers/my_aliases.rb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I use a Rails initializer file to define my custom aliases and local tweaks. Files from &lt;code class=&quot;highlighter-rouge&quot;&gt;config/initializers/&lt;/code&gt; are called when the app’s models have already been loaded so you can interact with all the app’s classes:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/initializers/my_aliases.rb&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter_attributes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;me&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my@email.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;pr1&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;slug: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-debug-project&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;pr2&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;slug: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-other-debug-project&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;refr&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;recent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:refresh_usage_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;center annotation&quot;&gt;Sample project-specific aliases and tweaks&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;I usually keep a dozen of three max four-letters aliases per project. They represent objects and methods I often interact with when debugging locally. Smuggling such global methods past code review would probably not be possible, hence the private aliases workaround.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;I regularly inspect the list of my Rails console commands for potential alias candidates using &lt;a href=&quot;https://github.com/pawurb/lazyme&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the lazyme gem&lt;/a&gt;. Correctly configured aliases can save you hundreds of keystrokes in just a single day of work. Your fingers will appreciate it.&lt;/p&gt;
</description>
        <pubDate>Tue, 08 Nov 2022 09:39:06 +0100</pubDate>
        <link>https://pawelurbanek.com/rails-debug-aliases</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-debug-aliases</guid>
      </item>
    
      <item>
        <title>The In-depth Guide to Caching ActiveRecord SQL Queries in Rails</title>
        <description>&lt;p&gt;Caching might seem a perfect solution to &lt;em&gt;“speed up”&lt;/em&gt; slow database queries. However, caching in Rails apps can be easily misused, leading to poor maintainability or even slower performance than without it. In this blog post, I’ll discuss the common pitfalls of caching SQL queries in Rails apps. I’ll also describe my toolkit for assessing the &lt;em&gt;cacheability&lt;/em&gt; of database queries and techniques for reducing the cost of caching infrastructure.&lt;/p&gt;

&lt;h2 id=&quot;railscache-101&quot;&gt;Rails.cache 101&lt;/h2&gt;

&lt;p&gt;Ruby on Rails offers a rich toolkit for caching different web application layers. Ranging from simple instance variables, through external in-memory cache shareable between processes, up to HTTP caching relying on headers and status codes. This blog post will focus on caching SQL-related data in separate in-memory storage.&lt;/p&gt;

&lt;p&gt;I won’t elaborate on choosing &lt;em&gt;the best&lt;/em&gt; in-memory database for your app’s cache. For projects that are just starting, &lt;a href=&quot;https://github.com/redis/redis-rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Redis&lt;/a&gt; is usually a decent choice. Once the app’s scale increases, it might be a good idea to separate the cache into a &lt;a href=&quot;https://github.com/petergoldstein/dalli&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Memcached with Dalli gem&lt;/a&gt; so that it does not clash with background worker data. Alternatively you can use two separate Redis instances.&lt;/p&gt;

&lt;p&gt;You can check your currently configured cache storage by running:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;For external in-memory storage, you should see:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;ActiveSupport::Cache::RedisCacheStore...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;or&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;ActiveSupport::Cache::MemCacheStore...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Please consult the documentation of respective gems for configuration details. You can also read &lt;a href=&quot;https://guides.rubyonrails.org/caching_with_rails.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the official Rails guide on caching&lt;/a&gt; for a detailed comparison between different cache storages supported in Rails. For production applications with non-trivial traffic, the external in-memory store should be the best choice in most cases. An advantage of an external in-memory cache is that it can be shared between different Ruby processes, e.g., multiple Heroku dynos. It means that time-consuming cache write will only be performed once, and all the other dynos can reuse the result.&lt;/p&gt;

&lt;h3 id=&quot;rails-cache-under-the-hood&quot;&gt;Rails cache under the hood&lt;/h3&gt;

&lt;p&gt;Before we discuss how and when we should cache SQL queries, let’s first take a closer look at what it means to &lt;em&gt;“cache”&lt;/em&gt; data using Rails framework. In this blog post, we’ll focus on the &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.cache&lt;/code&gt; module. It abstracts away the interface to the underlying data store and exposes simple methods for storing and retrieving cached data.&lt;/p&gt;

&lt;p&gt;You use it like that:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;important_value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_value_key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;important_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_value_key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 42&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;An often-used feature is the auto-expiry of cached values:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_value_key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;important_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_value_key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 42&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_value_key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; nil&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;A handy shortcut for automatically refreshing expired data is the &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch&lt;/code&gt; method:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_value_key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;important_value&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;It reads the cached value if present and refreshes it with the result of executing provided block if the selected key is expired.&lt;/p&gt;

&lt;p&gt;A useful trick is to leverage a &lt;a href=&quot;https://apidock.com/rails/ActiveRecord/Base/cache_key&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;cache_key&lt;/code&gt; method&lt;/a&gt; of an ActiveRecord query to ensure that it will be reused.&lt;/p&gt;

&lt;p&gt;You can also cache more complex objects:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_user&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cached_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_user&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cached_user&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Here’s where things are starting to get more interesting. &lt;code class=&quot;highlighter-rouge&quot;&gt;User&lt;/code&gt; is not a primitive value, but an &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord&lt;/code&gt; object built based on an SQL query results.&lt;/p&gt;

&lt;p&gt;So how come that the &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.cache&lt;/code&gt; can reconstruct the object without talking to the database? Introducing the &lt;a href=&quot;https://ruby-doc.org/core-3.1.2/Marshal.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Marshal&lt;/code&gt; module&lt;/a&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;dumped_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Marshal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dump&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dumped_user&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# &quot;\x04\bo:\tUser\x11:\x10@new_recordF:\x10@attr...&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This is a serialized representation of our &lt;code class=&quot;highlighter-rouge&quot;&gt;User&lt;/code&gt; object containing all the info necessary to restore it from text format back to the program memory. You can do it like that:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;restored_user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Marshal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dumped_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restored_user&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;object_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restored_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;object_id&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; false&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;center annotation&quot;&gt;Remember to avoid loading objects from user-provided input because it can result in a remote code execution vulnerability.&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;You can see that our restored object &lt;em&gt;equals&lt;/em&gt; our original user, but they are now different objects in memory since their &lt;code class=&quot;highlighter-rouge&quot;&gt;object_id&lt;/code&gt; attributes don’t match.&lt;/p&gt;

&lt;p&gt;Let’s now dig a bit deeper by peeking into the internals of how the cached object binary blobs are stored. If you’re using Redis as your cache database, you can read raw entries using a similar code snippet:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'redis'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;redis_client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;REDIS_URL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redis_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;important_user&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# &quot;\u0004\bo: ActiveSupport::Cache::Entry\n:\v@value\&quot;\u0002\xFC\u0003x\x9C\x9DU\u007Fo...&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You can notice that the resulting entry is different from output of using &lt;code class=&quot;highlighter-rouge&quot;&gt;Marshal.dump&lt;/code&gt; on the same user object. One notable difference is that &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.cache&lt;/code&gt; automatically compresses larger objects using the &lt;code class=&quot;highlighter-rouge&quot;&gt;Zlib::Deflate&lt;/code&gt; &lt;em&gt;stdlib&lt;/em&gt; module, but it still uses &lt;code class=&quot;highlighter-rouge&quot;&gt;Marshall.dump&lt;/code&gt; under the hood.&lt;/p&gt;

&lt;p&gt;Now that we’ve covered the basics let’s move on to speeding up our queries.&lt;/p&gt;

&lt;h2 id=&quot;how-to-find-sql-queries-worth-caching&quot;&gt;How to find SQL queries worth caching?&lt;/h2&gt;

&lt;p&gt;In theory, you could &lt;em&gt;“cache all the things”&lt;/em&gt; and Rails would scale…&lt;/p&gt;

&lt;p&gt;Nope :(&lt;/p&gt;

&lt;p&gt;When I work on &lt;a href=&quot;/#rails-performance-tuning&quot;&gt;speeding up a Rails application&lt;/a&gt;, I usually treat applying any backend caching technique as a &lt;em&gt;last resort&lt;/em&gt;. The most significant downside of caching is the additional complexity it introduces. By adding a caching layer, you instantly lose a &lt;em&gt;single source of truth&lt;/em&gt; trait of your SQL database. Any debugging effort now requires an analysis whether a potential stale cache issue could have affected it.&lt;/p&gt;

&lt;p&gt;I could keep on ranting about the downsides of cache layer… My approach is to avoid caching unless the potential cost is potentially worth it. That’s why in this tutorial, I focus on caching SQL queries. I think that it is one of the least complex backend caching techniques that can significantly speed up the application without all the usual downsides.&lt;/p&gt;

&lt;p&gt;The best part about SQL queries is that you can quickly find the ones worth caching and measure that expected gain before you deploy the change to production. One tool for finding queries that consume a significant amount of database resources is my &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras gem&lt;/a&gt; with its &lt;code class=&quot;highlighter-rouge&quot;&gt;calls&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;outliers&lt;/code&gt; methods. Check out my other blog post for more details on &lt;a href=&quot;/postgresql-fix-performance&quot;&gt;using rails-pg-extras to improve PostgreSQL performance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now that we know &lt;em&gt;which&lt;/em&gt; SQL queries to potentially cache, let’s finally discuss &lt;em&gt;how&lt;/em&gt; to do it.&lt;/p&gt;

&lt;h2 id=&quot;two-hard-things&quot;&gt;Two Hard Things&lt;/h2&gt;

&lt;p&gt;The challenging part of leveraging caching is not finding bottleneck queries, but rather deciding which queries can be cached without breaking the business logic. There are no straightforward answers since it differs per project. The best candidates for caching are always queries that are shared between multiple users. E.g. results of a popular or default search query.&lt;/p&gt;

&lt;p&gt;I’m not a fan of &lt;em&gt;“smart”&lt;/em&gt; cache expiry policies since it can add a hard-to-maintain complexity. In theory, you could base your cache expiry keys on the &lt;code class=&quot;highlighter-rouge&quot;&gt;updated_at&lt;/code&gt; attribute of an object. But, in Rails, it’s straightforward to modify objects bypassing all the callbacks magic, thus leading to hard-to-debug stale cache issues. And what about &lt;code class=&quot;highlighter-rouge&quot;&gt;updated_at&lt;/code&gt; of the object relations? Do you have to keep all the relations structures in sync with every state update now?… Even if the initial version of the &lt;em&gt;“smart”&lt;/em&gt; cache expiry policy might look straightforward, keeping the complexity at bay with the new features requested might not be possible.&lt;/p&gt;

&lt;p&gt;If you’re just starting with caching, usually, the simplest way to implement it is to add a fixed expiry threshold. The longer it is, the greater will be the performance benefit, but data displayed to users will be more outdated. Unfortunately, there are also no simple answers to configuring the correct cache expiry threshold. You can check out my other blog post for tips on &lt;a href=&quot;/rails-dynamic-config&quot;&gt;using dynamic config in Rails&lt;/a&gt; to easily tweak the values in production and observe results.&lt;/p&gt;

&lt;p&gt;But, as an example, let’s assume you’re optimizing an endpoint with traffic of 10 RPS. If all the requests generate the same slow SQL query, caching its results for 1 second would speed up ~90% of all traffic. Depending on your app’s traffic, even a small caching threshold could translate to huge performance and scalability benefits.&lt;/p&gt;

&lt;p&gt;It’s impossible to give universal advice on configuring the &lt;em&gt;“perfect”&lt;/em&gt; caching policies. So let’s instead move on to describing how to store cached data correctly.&lt;/p&gt;

&lt;h2 id=&quot;caching-sql-queries-in-rails&quot;&gt;Caching SQL queries in Rails&lt;/h2&gt;

&lt;p&gt;In the following examples, we’ll be working with this sample &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord&lt;/code&gt; model:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SELECT true FROM pg_sleep(1)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;to_json&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;nickname: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nickname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;created_at: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;slow&lt;/code&gt; scope is supposed to return 10 &lt;code class=&quot;highlighter-rouge&quot;&gt;User&lt;/code&gt; model objects after an artificial 1-second delay.&lt;/p&gt;

&lt;p&gt;Let’s now consider the following code snippet:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my_slow_query&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The big question is whether this &lt;em&gt;caching technique&lt;/em&gt; will improve performance?&lt;/p&gt;

&lt;p&gt;Nope :(&lt;/p&gt;

&lt;p&gt;The previously mentioned trick of inspecting raw cache values can help us understand why this is a bug:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redis_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my_slow_query&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;ActiveSupport::Cache::Entry\t:\v@valueo: User::ActiveRecord_Relation...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;center annotation&quot;&gt;If the cache entry is unreadable you can save it with &lt;code&gt;compress: false&lt;/code&gt; to simplify debugging.&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;We’ve accidentally cached an &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord::Relation&lt;/code&gt; object representing an SQL query that has not yet been executed. You can read my blog post about &lt;a href=&quot;/rails-load-async&quot;&gt;using &lt;code class=&quot;highlighter-rouge&quot;&gt;load_async&lt;/code&gt; API in Rails&lt;/a&gt; for more in-depth info on how and when ActiveRecord queries are triggered in Rails. Query object would only be executed after instantiating it from the cache, meaning that each request would suffer from the additional 1-second slowdown. It would not be possible to debug this issue without inspecting raw cache entry because any attempt to display &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord::Relation&lt;/code&gt; executes it obfuscating the original stored format.&lt;/p&gt;

&lt;p&gt;A better way to cache this query would look like that:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my_slow_query&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Appending &lt;code class=&quot;highlighter-rouge&quot;&gt;to_a&lt;/code&gt; forces query execution, and the resulting &lt;code class=&quot;highlighter-rouge&quot;&gt;User&lt;/code&gt; objects are serialized and stored in the cache database. Now reading the contents of the cache would not execute any SQL queries.&lt;/p&gt;

&lt;h2 id=&quot;how-to-spend-less-money-on-an-in-memory-cache&quot;&gt;How to spend less money on an in-memory cache?&lt;/h2&gt;

&lt;p&gt;But, here comes the tricky part. Do you need to store full-blown &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord&lt;/code&gt; objects? Let’s compare the size of cached ten full &lt;code class=&quot;highlighter-rouge&quot;&gt;User&lt;/code&gt; ActiveRecord objects with their JSON representation:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user_ar_objects&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user_json_objects&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:to_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;redis_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user_ar_objects&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 1721&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;redis_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user_json_objects&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 1064&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see, storing JSON representation takes almost half the space. Another benefit is the smaller memory usage required to instantiate JSON data compared to full-blown AR objects.&lt;/p&gt;

&lt;p&gt;Optimizing the size of your cached data is critical. Compared to the SSD disk space, memory storage is expensive. If you’re using Heroku, 1GB of &lt;a href=&quot;https://elements.heroku.com/addons/memcachier&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Memcachier storage&lt;/a&gt; costs $70/month, and 100GB would incur a monthly cost of $4000 (!!). You could reduce the price by spinning up a custom infrastructure, but this is just an example. The takeaway is that in-memory storage is expensive.&lt;/p&gt;

&lt;h3 id=&quot;how-to-minimize-cache-size-by-storing-only-ids&quot;&gt;How to minimize cache size by storing only IDs&lt;/h3&gt;

&lt;p&gt;There’s a way to significantly limit the amount of data you have to cache while maintaining &lt;em&gt;good enough&lt;/em&gt; performance. The slowdown in SQL queries is usually caused by complex search criteria that span multiple joined tables. But the outcome of the query are often objects from a single table. It means that in many cases, we should be able to store just the IDs of original query results and later reuse them to fetch objects without all the search logic overhead. Based on my tests saving only integer IDs takes ~10% of the in-memory space needed to store a JSON representation of an object with just a few attributes.&lt;/p&gt;

&lt;p&gt;Let’s see it in action:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;users_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my_slow_query_ids&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ids&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;We’re caching only user IDs and later use them to fetch objects efficiently.&lt;/p&gt;

&lt;p&gt;Unfortunately, this implementation has a sneaky bug. Maybe you’ve also learned the hard way that PostgreSQL does not preserve the order of IDs passed as a search param… (╯°□°）╯︵ ┻━┻&lt;/p&gt;

&lt;p&gt;It means that while returned objects would be correct, without an explicit &lt;code class=&quot;highlighter-rouge&quot;&gt;ORDER BY&lt;/code&gt;, their ordering would be random. This is critical if you’re paginating results because subsequent pages could return duplicates and omit some of the rows.&lt;/p&gt;

&lt;p&gt;One way to fix it is by reordering the objects with Ruby:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;users_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my_slow_query_ids&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ids&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort_by&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Based on my benchmarks fetching by IDs and sorting 1000 ActiveRecord objects in Ruby has an overhead of &lt;em&gt;~50ms&lt;/em&gt;. That’s usually an acceptable performance for fetching SQL data compared to an unoptimized database query. If you have to work with more objects, you should probably consider adding pagination.&lt;/p&gt;

&lt;p&gt;But, this technique forces us to execute the AR query object, which might not always be optimal. There are scenarios where you need the raw query to add eager loading or merge it with other queries. In this case, you can use an ugly way of custom sorting that’s supported by PostgreSQL (&lt;a href=&quot;https://stackoverflow.com/questions/12012574/postgres-order-by-values-in-in-list-using-rails-active-record&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;copy-paste source&lt;/a&gt;):&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_as_sorted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;)&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;relation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;joins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;JOIN (VALUES &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;,&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;) as x (id, ordering) ON &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.id = x.id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;relation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'x.ordering'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;relation&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;users_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my_slow_query_ids&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;expires_in: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;minute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ids&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;vi&quot;&gt;@users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_as_sorted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This method has a comparable overhead of &lt;em&gt;~50ms&lt;/em&gt; for 1000 objects. But, it produces a huge SQL query with the custom sorting order hardcoded. Always measure the performance impact if you’re about to merge it with other queries.&lt;/p&gt;

&lt;p&gt;Now our cached results are correctly sorted, taking up only a fraction of the original space. This technique allows you to aggressively cache more data without bloating the costs of your in-memory database.&lt;/p&gt;

&lt;p&gt;┳━┳ ヽ(ಠل͜ಠ)ﾉ&lt;/p&gt;

&lt;p&gt;That’s another advantage of caching just the critical data, i.e., bottleneck SQL results, instead of entire HTML views. Storage space needed is an order of magnitude smaller.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;The best way to implement caching is to avoid it. So, please always double-check if adding a database index cannot save you from developing a complex cache expiration strategy. But, if you have to do it, I hope some of the above tips will prove helpful.&lt;/p&gt;
</description>
        <pubDate>Mon, 17 Oct 2022 10:39:06 +0200</pubDate>
        <link>https://pawelurbanek.com/rails-active-record-caching</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-active-record-caching</guid>
      </item>
    
  </channel>
</rss>
