<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ruby and Rust developer specializing in backend performance - Paweł Urbanek</title>
    <description>Backend developer with over 10 years of experience. Specializing in Ruby on Rails, Rust, PostgreSQL, and backend performance.</description>
    <link>https://pawelurbanek.com/</link>
    <atom:link href="https://pawelurbanek.com/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Using LLMs and MCP to Debug PostgreSQL Performance in Rails</title>
        <description>&lt;p&gt;I’ve recently automated a large portion of my Rails performance audits. In this tutorial, I’ll describe how to configure an AI-powered PG performance debugging. We will cover using LLMs with custom MCP (model context protocol) &lt;a href=&quot;https://n8n.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;n8n&lt;/a&gt; integration. We will also discuss the legal and security implications of connecting AI to the production database.&lt;/p&gt;

&lt;h2 id=&quot;how-to-optimize-postgresql-performance-with-ai&quot;&gt;How to optimize PostgreSQL performance with AI?&lt;/h2&gt;

&lt;p&gt;I’m not sure if the current blogosphere needs yet another introduction to &lt;a href=&quot;https://modelcontextprotocol.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MCP&lt;/a&gt;. Instead, here’s a sneak peek of what we will be building in this tutorial:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;rails-pg-extras-mcp Slack n8n integration diagnose&quot; title=&quot;rails-pg-extras-mcp Slack n8n integration diagnose&quot; src=&quot;/assets/pg-mcp-diagnose-fb9adf197897d3995a0358a1cb7695bf200ffd7cf6055abfaf4c1648adba8a5c.png&quot; /&gt;
&lt;img class=&quot;center-image&quot; alt=&quot;rails-pg-extras-mcp Slack n8n integration explain analyze&quot; title=&quot;rails-pg-extras-mcp Slack n8n integration explain analyze&quot; src=&quot;/assets/pg-mcp-analyze-bd0212b714b9adeec57b85e8b2e84f4bf37f6fead0bcbff49a197f86bbeb6d8a.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TLDR&lt;/em&gt; Slack bot debugging PG performance.&lt;/p&gt;

&lt;p&gt;A Slack bot connected to the PostgreSQL database? Can it run arbitrary SQL queries? Does it send production data to OpenAI servers? Is it any good in nontrivial Rails apps? Will it yolo a &lt;code class=&quot;highlighter-rouge&quot;&gt;DROP TABLE&lt;/code&gt; on prod at 3 AM?&lt;/p&gt;

&lt;p&gt;Let’s answer all these questions.&lt;/p&gt;

&lt;h2 id=&quot;debugging-context-provided-by-the-pg-extras-gem&quot;&gt;Debugging context provided by the pg-extras gem&lt;/h2&gt;

&lt;p&gt;Copy-pasting queries from &lt;a href=&quot;https://github.com/heroku/heroku-pg-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;heroku-pg-extras&lt;/a&gt; one weekend turned out to be a good thing. I’ve been maintaining the &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; for over 6 years now. The project’s relative popularity helped me onboard multiple clients and earn a living as a Rails performance consultant for over four years.&lt;/p&gt;

&lt;p&gt;A significant part of my performance audits and subsequent retainer offering was interpreting the output of the pg-extras gem. It currently exposes ~40 methods. A solid entry point is the &lt;code class=&quot;highlighter-rouge&quot;&gt;diagnose&lt;/code&gt; method, which aggregates over a dozen checks to produce a report about the database health. By configuring the web interface, you can get a nice dashboard:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;rails-pg-extras web dashboard&quot; title=&quot;rails-pg-extras web dashboard&quot; src=&quot;/assets/pg-extras-diagnose-web-902027d5af0e434b394b0999b99d7d0ae31da1da2fee369315d9e0bd0a255890.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;rails-pg-extras web dashboard diagnose report&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Rails projects with a dynamic development pace and sizeable traffic are likely to discover new db-level inefficiencies every week. But, deep dive into metadata exposed by the pg-extras API can be tedious and repetitive process.&lt;/p&gt;

&lt;p&gt;Here’s where a brand new &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras-mcp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras-mcp gem&lt;/a&gt; makes an entrance.&lt;/p&gt;

&lt;h2 id=&quot;automating-pg-extras-performance-debugging-with-llms&quot;&gt;Automating pg-extras Performance Debugging with LLMs&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rails-pg-extras-mcp&lt;/code&gt; is a relatively simple (~300 boilerplate LOC total) wrapper on top of &lt;code class=&quot;highlighter-rouge&quot;&gt;rails-pg-extras&lt;/code&gt;. It exposes the API in a format digestible by LLMs. By connecting it to your AI model of choice, you can automate the repetitive task of hunting for inefficiencies in the database layer.&lt;/p&gt;

&lt;p&gt;This gem is a core component of the previously showcased Slack bot demo. Slack bot is just an arbitrary choice of interface, configured with the help of the &lt;a href=&quot;https://n8n.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;n8n&lt;/a&gt; platform:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;rails-pg-extras-mcp n8n integration schema&quot; title=&quot;rails-pg-extras-mcp n8n integration schema&quot; src=&quot;/assets/pg-mcp-n8n-e0ec040641c7d37fa6c6a9a951a8ebee949f2fc0b4690ccdc6320529cd78f523.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;rails-pg-extras MCP with Slack bot n8n schema&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;MCP protocol is universal, so you can use it with local Cursor IDE, Claude Code, OpenAI API, basically any modern LLM.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;rails-pg-extras-mcp Claude Desktop integration&quot; title=&quot;rails-pg-extras-mcp Claude Desktop integration&quot; src=&quot;/assets/pg-extras-mcp-claude-22e6f777e0fe69902c043a894111944c5ce27a6d53fa888c36bba3d4ee4cecae.jpg&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;Claude Desktop rails-pg-extras MCP integration&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;I like the Slack bot interface because it works well for team collaboration. Realistically, you cannot expect LLM to resolve PostgreSQL issues with 100% accuracy. However, the usual bottleneck of working with pg-extras is making sense of its verbose output. A raw output is often illegible:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Raw rails-pg-extras metadata output&quot; title=&quot;Raw rails-pg-extras metadata output&quot; src=&quot;/assets/raw-pg-extras-output-8d3c42a8b2c00d586fc0978f983834abd5d80b42a89a835101fb55fe42298c8f.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;Raw rails-pg-extras output&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;LLMs do a fantastic job, extracting useful info from raw data and reformatting it to vastly simplify human analysis. I’ve configured the demo Slack bot to only reply if mentioned by the &lt;code class=&quot;highlighter-rouge&quot;&gt;@n8n&lt;/code&gt; handle. A development team can discuss ongoing db issues on a dedicated channel and augment context by &lt;em&gt;predigested&lt;/em&gt; metadata from the LLM.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Slack bot PostgreSQL debugging discussion&quot; title=&quot;Slack bot PostgreSQL debugging discussion&quot; src=&quot;/assets/slack-bot-pg-discussion-8d80bfd959d63956d4f13d1a4fe03559716e33023fe599254c567caac27bf9c6.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;LLM-powered PG debugging session&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Please remember the Slack bot this is just a choice of interface. You can configure LLMs to email you weekly database healthcheck reports PDF, talk to the database directly from the Cursor IDE chatbox etc.&lt;/p&gt;

&lt;p&gt;So it looks pretty cool, but can you use it on production without summoning the GDPR police?&lt;/p&gt;

&lt;h2 id=&quot;legal-and-security-concerns-of-using-llms-with-production-data&quot;&gt;Legal and security concerns of using LLMs with production data&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;DISCLAIMER: I’m not a lawyer, and this is not legal advice. Please do your own research before exposing any sensitive data to LLMs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rails-pg-extras&lt;/code&gt; core component is &lt;a href=&quot;https://github.com/pawurb/ruby-pg-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ruby-pg-extras gem&lt;/a&gt;. It defines &lt;a href=&quot;https://github.com/pawurb/ruby-pg-extras/tree/main/lib/ruby_pg_extras/queries&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;em&gt;metadata queries&lt;/em&gt;&lt;/a&gt; with performance debug data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;METADATA&lt;/em&gt; is an critical keyword here. It means that you can use &lt;code class=&quot;highlighter-rouge&quot;&gt;rails-pg-extras&lt;/code&gt; MCP integration without giving LLMs access to ANY business logic-related tables. Additionally, even query argument values are not exposed. Here’s a sample output excerpt of the &lt;code class=&quot;highlighter-rouge&quot;&gt;calls&lt;/code&gt; method, which lists top bottlenecks:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SELECT first_name, last_name, email FROM customers WHERE email LIKE $1 ORDER BY last_name LIMIT $2     
SELECT * FROM orders WHERE order_date &amp;gt;= $1 ORDER BY order_date DESC LIMIT $2                          
SELECT * FROM customers WHERE loyalty_points &amp;gt; $1 ORDER BY loyalty_points DESC LIMIT $2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you can see, values are replaced by &lt;code class=&quot;highlighter-rouge&quot;&gt;$1&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;$2&lt;/code&gt; placeholders. It means that in the default &lt;code class=&quot;highlighter-rouge&quot;&gt;rails-pg-extras-mcp&lt;/code&gt; configuration, LLMs cannot read any sensitive data.&lt;/p&gt;

&lt;p&gt;To strengthen this assumption, you can configure a dedicated user with read access only to the metadata tables:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ROLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_viewer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NOLOGIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PASSWORD&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'your_password'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_viewer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CONNECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DATABASE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;your_db_name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;USAGE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SCHEMA&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_stat_statements&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_stat_activity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_locks&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_stat_user_indexes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_stat_all_tables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_stat_database&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_namespace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_relation_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;regclass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_indexes_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;regclass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_table_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;regclass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;EXECUTE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_total_relation_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;regclass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extras_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and configure the ENV value:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RAILS_PG_EXTRAS_MCP_DATABASE_URL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;postgres://extras_user:your_password@localhost:5432/your_db_name
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’m not sure if it’s enough to keep the GDPR inspectors happy. But, it’s an order of magnitude safer compared to letting LLMs roam free with a full-blown read-write db access.&lt;/p&gt;

&lt;h3 id=&quot;enabling-optional-explain-analyze-mode&quot;&gt;Enabling optional EXPLAIN ANALYZE mode&lt;/h3&gt;

&lt;p&gt;If you feel adventurous, you can toggle &lt;code class=&quot;highlighter-rouge&quot;&gt;PG_EXTRAS_MCP_EXPLAIN_ENABLED&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;PG_EXTRAS_MCP_EXPLAIN_ANALYZE_ENABLED&lt;/code&gt; flags. They add &lt;code class=&quot;highlighter-rouge&quot;&gt;explain&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;explain_analyze&lt;/code&gt; tools to the MCP interface:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;LLM using EXPLAIN ANALYZE to debug query performance&quot; title=&quot;LLM using EXPLAIN ANALYZE to debug query performance&quot; src=&quot;/assets/llm-explain-analyze-b88edd73d93e5c3a90be22f8e3de6cacf8a2ed54ae0c888e5fd501c210b3171b.png&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;center annotation&quot;&gt;LLM running EXPLAIN ANALYZE on a bottleneck query&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;This feature allows LLMs to run &lt;code class=&quot;highlighter-rouge&quot;&gt;EXPLAIN&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;EXPLAIN ANALYZE&lt;/code&gt; on selected SQL queries. Hand-crafting queries for such analysis by replacing &lt;code class=&quot;highlighter-rouge&quot;&gt;$1, $2&lt;/code&gt; placeholders with matching data can be a little tedious and time-consuming. LLMs do a fantastic job automating this process.&lt;/p&gt;

&lt;p&gt;A critical difference is that it requires read access to the underlying tables. And while &lt;code class=&quot;highlighter-rouge&quot;&gt;EXPLAIN&lt;/code&gt; outputs the only theoretical execution plan, &lt;code class=&quot;highlighter-rouge&quot;&gt;EXPLAIN ANALYZE&lt;/code&gt; runs the actual query and reports real execution metrics. It comes with an increased risk of potentially exposing sensitive data to the LLMs. Or even writing to the database if you forget to configure read-only access. I did put &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras-mcp/blob/main/lib/rails_pg_extras_mcp/validate_query.rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;some effort&lt;/a&gt; into preventing LLMs from injecting malicious SQL.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;LLM SQL injection attempt&quot; title=&quot;LLM SQL injection attempt&quot; src=&quot;/assets/explain-analyze-delete-5d1a94a7f261d9f84ad8aae6c5a4444e35704b41c05412e52557866cf4f19723.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;No hesitation&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;But I wouldn’t bet that a crafty LLM cannot sneak a &lt;code class=&quot;highlighter-rouge&quot;&gt;DROP TABLE&lt;/code&gt; SQL injection past these safeguards. If you have feedback on how this security layer could be improved, please get in touch.&lt;/p&gt;

&lt;h2 id=&quot;how-much-does-it-cost&quot;&gt;How much does it cost?&lt;/h2&gt;

&lt;p&gt;So far, I’ve had the most success with OpenAI &lt;code class=&quot;highlighter-rouge&quot;&gt;gpt-4o&lt;/code&gt; model. During the two weekends I spent testing the setup, sending ~200 Slack messages, I managed to rack up a whopping total of $1.56 in API charges.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;OpenAI API cost dashboard&quot; title=&quot;OpenAI API cost dashboard&quot; src=&quot;/assets/open-ai-api-cost-9beaa92c3cfda2c2b0d1286e5fcd40378806f9ef02fd3565ccb20465351c85c7.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Anthropic models seem significantly more expensive. &lt;code class=&quot;highlighter-rouge&quot;&gt;Claude Sonnet 4&lt;/code&gt; costs ~$0.05 per chat message, and &lt;code class=&quot;highlighter-rouge&quot;&gt;Claude Opus 4&lt;/code&gt; devoured $1 for a single performance analysis. I was unable to distinguish the difference in quality of output depending on the model, but I did not spend much time cross-testing them. An interesting strategy could be to use a pricey deep-thinking model for weekly performance reports, and a cheaper one for everyday chat usage.&lt;/p&gt;

&lt;p&gt;On top of that you’ll need &lt;a href=&quot;https://n8n.io/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;an n8n instance&lt;/a&gt; for ~$20 month, but you can also self-host it.&lt;/p&gt;

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

&lt;p&gt;Make sure to check out the &lt;a href=&quot;https://github.com/pawurb/rails-pg-extras-mcp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-pg-extras-mcp readme&lt;/a&gt; for info on how to use it with your Rails app. The project in an early POC stage, so contributions are welcome.&lt;/p&gt;

&lt;p&gt;A Slack bot, some LLM magic, a few hundred lines of glue code, and just like that… I’ve automated myself out of PG consulting gigs. The future looks scary. I’ll be off now to practice my plumbing skills.&lt;/p&gt;
</description>
        <pubDate>Tue, 22 Jul 2025 00:09:04 +0200</pubDate>
        <link>https://pawelurbanek.com/rails-postgresql-mcp</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-postgresql-mcp</guid>
      </item>
    
      <item>
        <title>My puts Debugging Workflow in Rails Apps</title>
        <description>&lt;p&gt;Puts debugging is the best. In this tutorial, I’ll walk you through my workflow for debugging Rails codebases using a custom toolkit inspired by Rust. We will also discuss how to smuggle your debug tools, through even the most rigorous code review process.&lt;/p&gt;

&lt;h2 id=&quot;why-and-how-to-puts-debug-ruby-code&quot;&gt;Why and how to puts debug Ruby code?&lt;/h2&gt;

&lt;p&gt;I’ve never been a fan of (or properly learned) using debug breakpoints. While I sometimes use &lt;a href=&quot;https://github.com/ruby/debug&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby debug&lt;/code&gt;&lt;/a&gt;, most of the time, &lt;em&gt;I’m a puts debugger&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;My favorite way to debug is via an automated test suite. I usually have the &lt;a href=&quot;https://github.com/AlexB52/retest&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;retest&lt;/code&gt; gem&lt;/a&gt; running in the background to automatically execute relevant tests on each file save. If I need to narrow the scope, I use a shortcut to copy the current filename with the line number and run a single test:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rs spec/models/user_spec.rb:42
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;center annotation&quot;&gt;&lt;code&gt;rs&lt;/code&gt; is a alias for &lt;code&gt;bundle exec rspec&lt;/code&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;So far, I’ve relied on quick-and-dirty print statements like these:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'!!!!!!!!!!!!!!'&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'first_user'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;first_user&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'second_user'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;second_user&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'!!!!!!!!!!!!!!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Please don’t tell me you haven’t written something like this before. It highlights a few issues with the default way to debug in Ruby.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;!!!!!!!!!!!!!!!&lt;/code&gt; - annotation is often needed. &lt;code class=&quot;highlighter-rouge&quot;&gt;p&lt;/code&gt; debug statements don’t stand out enough in a verbose test/logs output.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;p 'first_user', first_user&lt;/code&gt; - manually adding labels is necessary to identify which debug statement is which.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;p&lt;/code&gt; method calls are hard to clean up later since they might already exist elsewhere in the codebase.&lt;/p&gt;

&lt;h2 id=&quot;introducing-dbg-rb---a-simple-ruby-puts-debugging-helper&quot;&gt;Introducing &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; - a simple Ruby puts debugging helper&lt;/h2&gt;

&lt;p&gt;Recently, I’ve been writing more Rust than Ruby. I really came to appreciate its built-in &lt;a href=&quot;https://doc.rust-lang.org/std/macro.dbg.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;dbg!&lt;/code&gt; macro&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;derive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Debug&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;struct&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;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&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;fn&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&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;let&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;User&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;user1&quot;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.to_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;password1&quot;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.to_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&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;nd&quot;&gt;dbg!&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;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// [src/main.rs:15:5] user = User {&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;//     username: &quot;user1&quot;,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;//     password: &quot;password1&quot;,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;//     active: true,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It produces a verbose, source-located output, together with the name of the variable. Coming back to Ruby, it felt odd that such a simple and handy helper isn’t available out of the box.&lt;/p&gt;

&lt;p&gt;Hence, the birth of the &lt;a href=&quot;https://github.com/pawurb/dbg-rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; gem&lt;/a&gt;. It’s my attempt to recreate the Rust &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg!&lt;/code&gt; macro in Ruby:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;dbg&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;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;:as_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;dbg-rb debug output&quot; title=&quot;dbg-rb debug output&quot; src=&quot;/assets/dbg_base-d979825f94e67009a5af5d62fc63d47ebeb8bb92cd7d64a66bb1c87cd9c2c292.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It prints the filename, line number, and source expression — all with colored output for quick scanning. It also works for method and keyword arguments. For me, knowing the source location and seeing labeled variables names makes debugging faster and far more pleasant to work with. What’s more, the &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg&lt;/code&gt; method is unique, so it’s trivial to grep and clean up after use.&lt;/p&gt;

&lt;p&gt;For more details and configuration options, check out the documentation in the &lt;a href=&quot;https://github.com/pawurb/dbg-rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;inline-setup&quot;&gt; How to use dbg-rb in Rails without adding it to the Gemfile?&lt;/h2&gt;

&lt;p&gt;I find &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; useful in my everyday Rails work. But I understand that not all the devs will share this enthusiasm. When doing performance audits, I needed a way to use this tool without cluttering the dependency list — especially in teams that are strict about gem additions.&lt;/p&gt;

&lt;p&gt;In theory it’s possible to add &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; to the IRB config to include it in the Rails console context. But it has little value; I rarely debug directly in the console. I use a test suite for that. It means that &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; has to be available in the &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle exec&lt;/code&gt; context. But how can you add a gem to the Gemfile without modifying it?&lt;/p&gt;

&lt;p&gt;In theory, I could use a local development branch with a custom Gemfile, but cherry-picking changes before submitting a PR is cumbersome.&lt;/p&gt;

&lt;p&gt;Here’s a &lt;em&gt;hack&lt;/em&gt; that I came up with. It’s similar to &lt;a href=&quot;/rails-debug-aliases&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my previous post on Rails debug aliases&lt;/a&gt;. You can inject &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; into a Rails app via a local initializer file that’s ignored by Git globally.&lt;/p&gt;

&lt;p&gt;Start by creating the file with the following contents:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;~/.gitignore_global&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;config/initializers/dbg_rb.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now configure your git to use it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; core.excludesfile ~/.gitignore_global
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next you can add an inlined &lt;em&gt;lite&lt;/em&gt; version of the &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; file to your Rails app initializers folder:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;wget https://raw.githubusercontent.com/pawurb/dbg-rb/refs/heads/main/inline/dbg_rb.rb &lt;span class=&quot;nt&quot;&gt;-O&lt;/span&gt; config/initializers/dbg_rb.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it! You can now use &lt;code class=&quot;highlighter-rouge&quot;&gt;dbg&lt;/code&gt; to debug local variables and parameters with automatically appended labels:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;dbg&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; 
&lt;span class=&quot;c1&quot;&gt;# [spec/user_spec.rb:20] user = #&amp;lt;User id: 1, team_id: 1, slack_id: &quot;develop&quot;, pseudonym: ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;None of these steps leaves a trace in the git diff, so you can apply it to any Ruby/Rails project without a code review process. Debug tools like this are safe for local use. But be cautious if you’re introducing less obvious techniques — always consult your team before adding anything bypassing standard workflows.&lt;/p&gt;

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

&lt;p&gt;I find the additional context provided by &lt;a href=&quot;https://github.com/pawurb/dbg-rb&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;dbg-rb&lt;/code&gt; gem&lt;/a&gt; helpful for non-trivial debugging sessions. And if you’re a &lt;em&gt;“vibe debugger”&lt;/em&gt;, feeding this extra info to the LLMs increases your chance to quickly produce a correct answer. I wish a similar tool was available by default like in Rust. Unfortunatelly, this &lt;a href=&quot;https://bugs.ruby-lang.org/issues/20987&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ruby feature proposal&lt;/a&gt; has received little traction so far. If you have suggestions for improving dbg-rb — feedback, PRs, and bug reports are always appreciated.&lt;/p&gt;
</description>
        <pubDate>Mon, 19 May 2025 07:58:21 +0200</pubDate>
        <link>https://pawelurbanek.com/rails-puts-debug</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-puts-debug</guid>
      </item>
    
      <item>
        <title>Lessons Learned Migrating my SAAS to Rails 8</title>
        <description>&lt;p&gt;My side project &lt;a href=&quot;https://abot.app&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Abot for Slack&lt;/a&gt;, has been around for ~7 years, i.e., since Rails &lt;em&gt;5.1&lt;/em&gt;. It’s now yielding passive income while running on an almost complete autopilot. But I’m still keeping its dependencies up-to-date. The recent migration to Rails 8 was arguably the most impactful in the app’s lifetime. In this blog post, I’ll describe features introduced in the newest version of the framework and how they affected my project.&lt;/p&gt;

&lt;h2 id=&quot;kamal-vs-dokku&quot;&gt;Kamal vs Dokku&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;TLDR&lt;/em&gt; initial impressions migrating Abot from &lt;a href=&quot;https://dokku.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dokku&lt;/a&gt; to &lt;a href=&quot;https://kamal-deploy.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Kamal&lt;/a&gt;: it’s better, apart from one major flaw.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/profitable-slack-bot-rails&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Abot started as a weekend project&lt;/a&gt; deployed to Heroku free dynos. Later, I migrated it to AWS EC2 with the RDS PostgreSQL database using &lt;a href=&quot;https://dokku.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dokku&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Initial dokku setup&quot; title=&quot;Initial dokku setup&quot; src=&quot;/assets/dokku-gitlog-635bdb61b04ba1b4bb268dfcadb071cbfa963b767601abbba265dffc2ec7d7d6.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;The initial Dokku setup long long time ago&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Dokku requires a bit more dev-ops expertise than PAAS solutions, but after the initial setup, it offers a workflow almost identical to Heroku. I’ve used Dokku for over 4 years, and it provided a seamless deployment experience with very few glitches along the way. This is one of the OS projects that I &lt;a href=&quot;https://opencollective.com/dokku&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;regularly donate to&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But the times are changing, and &lt;a href=&quot;https://kamal-deploy.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Kamal&lt;/a&gt; is a fancy new tool that everyone is talking about.&lt;/p&gt;

&lt;p&gt;Kamal’s great advantage is a declarative one-file config, providing an instant overview of the current state of the project. With Dokku, similar insight is scattered across several commands.&lt;/p&gt;

&lt;p&gt;Another benefit is seamless deployment setup. Just point Kamal to a barebones VPS, and everything gets deployed via a single &lt;code class=&quot;highlighter-rouge&quot;&gt;kamal setup&lt;/code&gt; command. Dokku requires manual installation on the server unless configured with a custom Ansible playbook or similar tool.&lt;/p&gt;

&lt;p&gt;Yet another win for Kamal is support for multi-server architecture. With Dokku, you’re limited to single server deployments, so you have to fit all the services, including databases, caches, etc., on a single VPS.&lt;/p&gt;

&lt;p&gt;One thing that’s missing from Kamal is the built-in Nginx integration. Dokku provides a customizable Nginx template allowing for fine-tuned configuration of headers, SSL, compression, etc. With Kamal, you’d need to manually configure Nginx in front of the proxy to achieve the same effect. Apparently, the &lt;a href=&quot;https://github.com/basecamp/thruster&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;thruster gem&lt;/a&gt; can provide similar features as Nginx, but I have not explored it yet. In the end I’ve reimplemented each of the Nginx config tweaks directly in Rails or Cloudflare.&lt;/p&gt;

&lt;p&gt;But here comes the previously mentioned &lt;em&gt;major flaw&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;kamal-registry-based-deployments&quot;&gt;Kamal registry-based deployments&lt;/h3&gt;

&lt;p&gt;Dokku uses a &lt;em&gt;git-based&lt;/em&gt; deployment flow, i.e. each &lt;code class=&quot;highlighter-rouge&quot;&gt;git push&lt;/code&gt; triggers a new docker image build combining the base image with app-specific &lt;code class=&quot;highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;. A new docker image is built directly on the target VPS and kept locally.&lt;/p&gt;

&lt;p&gt;Kamal requires a nonoptional &lt;code class=&quot;highlighter-rouge&quot;&gt;registry.server&lt;/code&gt; (defaults to &lt;code class=&quot;highlighter-rouge&quot;&gt;&quot;hub.docker.com&quot;&lt;/code&gt;), &lt;code class=&quot;highlighter-rouge&quot;&gt;registry.username&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;registry.password&lt;/code&gt; config values. These are credentials for one of the commercial (DockerHub, GitHub, Digital Ocean) or custom docker registry service. It’s possible to &lt;a href=&quot;https://distribution.github.io/distribution/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;spin up your own registry&lt;/a&gt;, but it’s cumbersome. Built images are pushed to the registry and later downloaded by a target host.&lt;/p&gt;

&lt;p&gt;An issue is that the default Kamal configuration exposes Rails application source code in a public image hosted on Docker Hub. In theory, it could leak nonpublic projects if someone followed the Kamal tutorial without analyzing each step. I wish the Kamal team was more explicit about this behavior in the docs and learning materials.&lt;/p&gt;

&lt;p&gt;A way to prevent exposing project source code is manually creating the private image up-front or changing the default visibility &lt;a href=&quot;https://hub.docker.com/repository-settings/default-privacy&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;in DockerHub settings&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;center-image&quot; alt=&quot;Docker hub privacy settings&quot; title=&quot;Docker hub privacy settings&quot; src=&quot;/assets/docker-hub-privacy-c6275aaae649ec547ece349dd21417e9857a73180024f415dd95fdd3bd2f2ce9.png&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;center annotation&quot;&gt;DockerHub image privacy settings&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;A free DockerHub account can host only a single private image. Even &lt;a href=&quot;https://www.docker.com/pricing/#monthly&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the $11/month plan&lt;/a&gt; offers only 5GB storage, so with image sizes at ~1GB it might not be enough. A $16/month offer with 50GB space for private images could be necessary for many users. It might not seem expensive. But a choice between exposing source code or incurring monthly costs could be discouraging for someone just exploring the Rails stack.&lt;/p&gt;

&lt;p&gt;Fortunately, &lt;a href=&quot;https://github.com/basecamp/kamal/discussions/510#discussioncomment-11291136&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;there are rumors&lt;/a&gt; about the Kamal team working on a way to deploy containers, without relying on 3rd party commercial services. In my opinion, this change would significantly enhance Kamal’s appeal.&lt;/p&gt;

&lt;h2 id=&quot;goodbye-redis&quot;&gt;Goodbye Redis&lt;/h2&gt;

&lt;p&gt;Since I started working in Rails around a decade ago, the &lt;em&gt;Redis + Sidekiq&lt;/em&gt; combo was the de-facto standard for most projects. Rails 8 challenges this status quo by defaulting to the &lt;a href=&quot;https://github.com/rails/solid_queue&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;solid_queue gem&lt;/a&gt; for background processing. Solid Queue uses SQL database storage instead of Redis. Together with &lt;a href=&quot;https://github.com/rails/mission_control-jobs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mission_control-jobs UI&lt;/a&gt; it’s a drop-in replacement for Sidekiq. It means that we can now get rid of another process and dependency to maintain.&lt;/p&gt;

&lt;p&gt;Caching was another area where I’ve seen widespread use of Redis. On the side node, reusing the same Redis instance for both Sidekiq and caching has always been a fun source of sneaky bugs when both processes’ data interfere despite the namespaces…&lt;/p&gt;

&lt;p&gt;(╯°□°)╯︵ ┻━┻&lt;/p&gt;

&lt;p&gt;Past traumas aside, with the introduction of &lt;a href=&quot;https://github.com/rails/solid_cache&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;solid_cache gem&lt;/a&gt; modern Rails apps can remove Redis from their stack, simplifying the infrastructure. While conducting &lt;a href=&quot;/optimize-rails-performance&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my Rails performance audits&lt;/a&gt;, I’ve seen projects where Redis was responsible for a significant portion of monthly infra costs. Migrating to SQL-based solutions can result in a major cost reduction for many Rails projects. Also, as per Rails 8 marketing: &lt;em&gt;“you can now cache weeks instead of hours worth of data”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a breakdown of monthly AWS cost for &lt;a href=&quot;https://aws.amazon.com/elasticache/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ElastiCache Redis&lt;/a&gt; with a specified RAM vs &lt;a href=&quot;https://aws.amazon.com/rds/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RDS database&lt;/a&gt; disk space:&lt;/p&gt;

&lt;table style=&quot;width:100%&quot;&gt;
  &lt;tr&gt;
    &lt;th&gt;Size&lt;/th&gt;
    &lt;th&gt;Redis RAM&lt;/th&gt;
    &lt;th&gt;RDS disk space&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;1.3GB&lt;/td&gt;
    &lt;td&gt;~$23&lt;/td&gt;
    &lt;td&gt;~$0.16&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;6.3GB&lt;/td&gt;
    &lt;td&gt;~$110&lt;/td&gt;
    &lt;td&gt;~$0.78&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;52GB&lt;/td&gt;
    &lt;td&gt;~$900&lt;/td&gt;
    &lt;td&gt;~$6.5&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;210GB&lt;/td&gt;
    &lt;td&gt;~$3300&lt;/td&gt;
    &lt;td&gt;~$26&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;div class=&quot;annotation center&quot;&gt;Size values were chosen to find matching ElastiCache instance&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;RDS incurs additional db instance costs, but on a larger, scale they will be insignificant. Cache-specific databases execute only simple queries over well-indexed tables, i.e. no joins or &lt;em&gt;seq scans&lt;/em&gt;. That’s why caching database CPU/RAM can usually be much smaller than primary. But, workload and traffic patterns differ for each project, so please run your benchmarks before making any production infra changes. More cost-aware projects can also squeeze both databases on a single RDS instance.&lt;/p&gt;

&lt;p&gt;I did calculations for ElastiCache, so you could slightly reduce the cost by running Redis on EC2 instances. But RAM will always be an order of magnitude more expensive than disk space.&lt;/p&gt;

&lt;p&gt;I’ve seen comments that migrating to &lt;em&gt;“non-memory”&lt;/em&gt; cache storage will result in a performance drop. So let’s see the numbers in action.&lt;/p&gt;

&lt;h3 id=&quot;measuring-non-memory-caching-performance&quot;&gt;Measuring non-memory caching performance&lt;/h3&gt;

&lt;p&gt;I’ve run the following benchmark script against different Rails cache stores:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;keys&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;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;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;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;mi&quot;&gt;2000&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;test-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&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;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;n&quot;&gt;key&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;keys&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&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;n&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sample&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;/div&gt;&lt;/div&gt;

&lt;p&gt;Please take this benchmark with a grain of salt. It’s a straightforward script that does not simulate how the cache store behaves under a constant load or when there’s more data.&lt;/p&gt;

&lt;p&gt;It distributes read and write operations in 80/20 relation. Here’s the result for different store types:&lt;/p&gt;

&lt;table style=&quot;width:100%&quot;&gt;
  &lt;tr&gt;
    &lt;th&gt;Store&lt;/th&gt;
    &lt;th&gt;Total time&lt;/th&gt;
    &lt;th&gt;Avg. time&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;FileStore EBS&lt;/td&gt;
    &lt;td&gt;20.0s&lt;/td&gt;
    &lt;td&gt;~4ms&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Redis/Memcache&lt;/td&gt;
    &lt;td&gt;20.1s&lt;/td&gt;
    &lt;td&gt;~4ms&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;FileStore EFS&lt;/td&gt;
    &lt;td&gt;22.4s&lt;/td&gt;
    &lt;td&gt;~4.5ms&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;SolidCache SQLite&lt;/td&gt;
    &lt;td&gt;24.4s&lt;/td&gt;
    &lt;td&gt;~4.8ms&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;SolidCache PG&lt;/td&gt;
    &lt;td&gt;27.05s&lt;/td&gt;
    &lt;td&gt;~5.4ms&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;For me, the main takeaway from the above table is that in the context of web applications, the caching layer is &lt;em&gt;fast enough&lt;/em&gt;, regardless of the underlying store type. The amount of data that can be cached is vastly more important than sub-millisecond performance improvements.&lt;/p&gt;

&lt;p&gt;Although the &lt;em&gt;in-memory&lt;/em&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;Redis/Memcache&lt;/code&gt; should be the fastest, they are on par with the &lt;code class=&quot;highlighter-rouge&quot;&gt;File Store EBS&lt;/code&gt;. The reason is probably the lack of networking overhead for local file storage.&lt;/p&gt;

&lt;p&gt;Another interesting observation is that the &lt;code class=&quot;highlighter-rouge&quot;&gt;FileStore&lt;/code&gt; is consistently &lt;em&gt;~10-20%&lt;/em&gt; faster than &lt;code class=&quot;highlighter-rouge&quot;&gt;SolidCache SQLite&lt;/code&gt;. It makes sense as SQLite is a layer of abstraction on top of the file system, so it incurs additional overhead. I’m not 100% sure if this metric will hold true for systems under production-like load. But if you want to squeeze out a few more microseconds then the the good ol’ &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveSupport::Cache::FileStore&lt;/code&gt; instead of SQLite could be a better choice.&lt;/p&gt;

&lt;p&gt;Please remember that &lt;a href=&quot;https://aws.amazon.com/ebs/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AWS EBS&lt;/a&gt; (even &lt;code class=&quot;highlighter-rouge&quot;&gt;gp3&lt;/code&gt;) volumes are notoriously slow. When running the benchmark locally on an MBP, I’m observing ~3x better performance for &lt;code class=&quot;highlighter-rouge&quot;&gt;FileStore&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;SolidCache SQLite&lt;/code&gt;. So if you want to maximize the SQLite and cache performance, choosing a VPS provider with high-speed NVMe disks is probably worth it.&lt;/p&gt;

&lt;p&gt;While we’re at data stores, I would like to highlight the &lt;a href=&quot;https://aws.amazon.com/efs/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AWS EFS file system&lt;/a&gt;. It’s EBS-equivalent but can be shared between multiple EC2 instances. It’s ~10x more expensive than EBS, but using it instead of PostgreSQL would mean saving money on database instances. It could be an interesting choice for projects using SQLite with multi-server architecture or for leveraging a shared file-based caching layer. I’m currently working on a blog post covering more details on using EFS with Rails. You can &lt;a href=&quot;https://x.com/pawelurbanekcom&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;follow&lt;/a&gt; or &lt;a class=&quot;link-grey&quot; href=&quot;https://eepurl.com/dhuFg5&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;subscribe&lt;/a&gt; to get notified when it’s live.&lt;/p&gt;

&lt;h3 id=&quot;using-a-custom-compressor-for-rails-cache&quot;&gt;Using a custom compressor for Rails cache&lt;/h3&gt;

&lt;p&gt;Another exciting feature introduced in Rails 8 is support for custom cache compressors. I’ve built 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; around this idea, so it’s awesome to see it upstream. The feature has not made it to the Rails guides yet, but hopefully, &lt;a href=&quot;https://github.com/rails/rails/pull/53036&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this PR&lt;/a&gt; will get merged sooner or later. As of now &lt;code class=&quot;highlighter-rouge&quot;&gt;compressor&lt;/code&gt; config is described only in &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveSupport::Cache::Store&lt;/code&gt; docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve tested a few compression algorithms when working on rails-brotli-cache (&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;benchmark&lt;/a&gt;). Choosing &lt;em&gt;“the best one”&lt;/em&gt; is a matter of tradeoffs between performance and compression ratio. But &lt;a href=&quot;https://github.com/SpringMT/zstd-ruby&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;zstd-ruby&lt;/a&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;level: 10&lt;/code&gt; config is a &lt;em&gt;quick &amp;amp; easy&lt;/em&gt; choice, offering speed and compression superior to both &lt;code class=&quot;highlighter-rouge&quot;&gt;gzip&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;brotli&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;lib/zstd_compressor.rb&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;zstd-ruby&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ZSTDCompressor&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;deflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&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;no&quot;&gt;Zstd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;level: &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;k&quot;&gt;end&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;inflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&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;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x78&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Zlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Zstd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;decompress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&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;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;config/production.rb&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&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;:solid_cache_store&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;compressor: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ZSTDCompressor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Applying this config should improve caching read/write performance by &lt;em&gt;~30%&lt;/em&gt; and compression ratio by &lt;em&gt;~20%&lt;/em&gt;. If you want to use this feature make sure to include:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&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;load_defaults&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;7.1&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;in &lt;code class=&quot;highlighter-rouge&quot;&gt;config/application.rb&lt;/code&gt;. This change is backward compatible, so you can start using the new algorithm without clearing the current cache contents.&lt;/p&gt;

&lt;h2 id=&quot;sqlite-vs-postgresql&quot;&gt;SQLite vs PostgreSQL&lt;/h2&gt;

&lt;p&gt;In Rails 8 SQLite becomes a &lt;em&gt;first-class&lt;/em&gt; citizen. This &lt;a href=&quot;https://www.youtube.com/watch?v=wFUy120Fts8&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;talk by Stephen Margheim&lt;/a&gt; provides an in-depth look at the changes introduced and explains how SQLite’s support and performance improved.&lt;/p&gt;

&lt;p&gt;I briefly considered switching Abot’s primary database to SQLite instead of PostgreSQL. I’d love to cut the monthly RDS costs, but I’m not comfortable keeping the business-critical data on the server. &lt;em&gt;“cattle not pets”&lt;/em&gt; they said. I’m used to regularly nuking and reprovisioning EC2 instances, so the chances of accidentally wiping production data skyrocket with SQLite.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/benbjohnson/litestream&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;litestream&lt;/a&gt; is an awesome tool for continuously backing up SQLite. But I’d rather not bet the project’s existence on it working seamlessly.&lt;/p&gt;

&lt;p&gt;Another disadvantage of SQLite vs. PG is the lack of advanced monitoring. Tools like &lt;a href=&quot;https://github.com/ankane/pghero&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PgHero&lt;/a&gt;, (&lt;em&gt;shameless plug alert!&lt;/em&gt;) &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&lt;/a&gt;, or &lt;a href=&quot;https://github.com/pawurb/pg-locks-monitor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pg-locks-monitor&lt;/a&gt; provide detailed insights into what’s going under the hood in PostgreSQL.&lt;/p&gt;

&lt;p&gt;With SQLite going mainstream in the Rails world, hopefully, open source will soon catch up, and we’ll have more ways to analyze it. I’ve started building &lt;a href=&quot;https://github.com/pawurb/rails-sqlite-extras&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;rails-sqlite-extras&lt;/a&gt;. But I don’t have much SQLite exp, so ideas on how to improve it are welcome.&lt;/p&gt;

&lt;p&gt;For now, I’m happy to keep Abot’s solid queue and cache databases on SQLite and primary on PostgreSQL.&lt;/p&gt;

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

&lt;p&gt;Rails 8 is a decent step forward. It makes things cheaper, faster, more productive. There are some drawbacks, like the Kamal mandatory registry, but I hope it will be sorted out soon.&lt;/p&gt;
</description>
        <pubDate>Tue, 14 Jan 2025 08:29:37 +0100</pubDate>
        <link>https://pawelurbanek.com/rails-8-features</link>
        <guid isPermaLink="true">https://pawelurbanek.com/rails-8-features</guid>
      </item>
    
      <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;'link[rel=&quot;alternate&quot;][type=&quot;application/rss+xml&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;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/channel_id=&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;([^&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&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;/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;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;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;p&gt;&lt;em&gt;Update:&lt;/em&gt; I’ve wrapped this implementation in a easy-to-use gem &lt;a href=&quot;https://github.com/pawurb/pg-locks-monitor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pg-locks-monitor&lt;/a&gt;.&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>
    
  </channel>
</rss>
