<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Meg528</title>
    <description>The latest articles on DEV Community by Meg528 (@meg528).</description>
    <link>https://dev.to/meg528</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1096069%2Fdad12650-7cea-4bd0-a782-2c3548aebcfe.jpeg</url>
      <title>DEV Community: Meg528</title>
      <link>https://dev.to/meg528</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/meg528"/>
    <language>en</language>
    <item>
      <title>RLS sounds great until it isn't</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Mon, 11 May 2026 16:06:32 +0000</pubDate>
      <link>https://dev.to/planetscale/rls-sounds-great-until-it-isnt-4d5p</link>
      <guid>https://dev.to/planetscale/rls-sounds-great-until-it-isnt-4d5p</guid>
      <description>&lt;p&gt;&lt;em&gt;By Josh Brown&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When you leave your house, go to sleep, or go do work in the yard, you lock your door. Maybe you have a gate or fence you lock too. Without these, anyone can waltz into your house and snoop around.&lt;/p&gt;

&lt;p&gt;Row Level Security (RLS) can be attractive to developers for numerous reasons, but the foot-guns and gotchas in RLS often outweigh the benefits. You probably want to keep your doors locked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friends and family: Managing access
&lt;/h2&gt;

&lt;p&gt;RLS for Postgres lets administrators define security policies in their database, instead of the application layer. Let's imagine your house is your database, and the rows, tables, and data are like the things inside.&lt;/p&gt;

&lt;p&gt;When your friends or family come over, you give them keys to every drawer they are allowed to have access to. Maybe everyone gets access to the silverware, but only the family can access your laundry room.&lt;/p&gt;

&lt;p&gt;This is similar to how policies work in RLS. The rules for who gets which keys are your policies. If a user passes a policy rule (has the key) then they are allowed to access the data. At a very small scale, this can seem like a great idea. Anyone can access your database however they want and your policies ensure they aren't seeing things they shouldn't.&lt;/p&gt;

&lt;p&gt;Testing and scaling these policies as your database grows becomes near impossible. For every new feature in your application, you must ensure your RLS policies are protecting the correct rows. Remembering to add these policies can be cumbersome, especially when they need to be manually synced to your codebase.&lt;/p&gt;

&lt;p&gt;RLS fundamentally exists to protect your data. If you mess up even a single policy however, your data becomes exposed. Managing access in the same location your code lives is much easier than remembering to write a new policy every time a new table, column, or feature is added to your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The party: Managing connections
&lt;/h2&gt;

&lt;p&gt;Postgres uses a process-per-connection architecture. Each new user connecting to your database directly with their role is like a new person coming into your house. At first it's fine, but once you have 100 people it gets crowded pretty quick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pscale.link/rlC77qY" rel="noopener noreferrer"&gt;PgBouncer&lt;/a&gt; is a connection pooler that reuses a small number of direct connections to your database while letting many clients connect to it. When using PgBouncer with RLS, you lose the upstream identity of the client.&lt;/p&gt;

&lt;p&gt;The traditional way of solving this is using local variables instead of roles to define RLS policies. You define a policy that reads from a session-local variable instead of checking the Postgres role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;user_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.tenant_id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then wrap every transaction in your application to set that variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1234'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires a lot of extra application code to manage all the different local variables attached to each and every transaction (1). If &lt;code&gt;SET LOCAL&lt;/code&gt; is omitted, &lt;code&gt;current_setting()&lt;/code&gt; returns an empty string or throws an error depending on how your policy is written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annoying neighbor: Attack Surface
&lt;/h2&gt;

&lt;p&gt;You go out to get your mail and you find your neighbor standing over your mailbox trying to open it over and over. You try to tell them that one is yours and to let you in, but they are having none of it. Now you have to sit and wait until they get bored and figure out they don't have the right key.&lt;/p&gt;

&lt;p&gt;RLS acts like an extra &lt;code&gt;WHERE&lt;/code&gt; clause appended to your queries. Unless the user lacks read permission on a table, their queries will still run even if no data is returned. On complex joins or queries lacking indexes, this can hurt database performance.&lt;/p&gt;

&lt;p&gt;If a malicious user starts retrying a query over and over, RLS will make sure they don't see any data, but cannot stop them from running the query itself. Relying on RLS to completely protect your tables burns valuable CPU cycles and can potentially starve your other, honest users.&lt;/p&gt;

&lt;p&gt;Any user of your application, particularly in situations where you do not have sufficient rate limiting in place, can DDoS your database simply by hitting an API endpoint. This is preventable by checking authentication to see if a user is allowed to run a query, without relying on RLS to manage your security for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  A large keyring: Performance Implications
&lt;/h2&gt;

&lt;p&gt;Every time your friend goes to get a Diet Coke, they need to find the fridge key on their very large key chain. This wastes valuable time sifting through all the different keys and trying each one, so instead they mark the key so it's easier to find next time they go to the fridge.&lt;/p&gt;

&lt;p&gt;RLS policies are generally executed per row (2), meaning any function or complex logic will run for each row scanned. This can be solved by wrapping functions into subqueries. Setting up a simple benchmark, we can see the difference between RLS, RLS cached, and with RLS disabled. If you want to try it yourself, you can use &lt;a href="https://github.com/planetscale/rls-latency-benchmark" rel="noopener noreferrer"&gt;this benchmark repository&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;For this benchmark, we tested 5 different setups. Two different functions that are called from two different policies, and one without RLS at all.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;RLS with a &lt;code&gt;VOLATILE&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;RLS with a &lt;code&gt;STABLE&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;RLS with a &lt;code&gt;VOLATILE&lt;/code&gt; function + cache&lt;/li&gt;
&lt;li&gt;RLS with a &lt;code&gt;STABLE&lt;/code&gt; function + cache&lt;/li&gt;
&lt;li&gt;No RLS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A volatile function is defined with the keyword &lt;code&gt;VOLATILE&lt;/code&gt; that tells Postgres the function may modify data or return different values upon successive calls. This is the default mode for a new function in Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;get_current_role&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;
&lt;span class="k"&gt;VOLATILE&lt;/span&gt;
&lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other option is to use &lt;code&gt;STABLE&lt;/code&gt; in our function definition. Stable functions cannot modify data, and are expected to return the same value for successive calls within the same transaction. When using RLS however, Postgres does not cache the value when evaluating the policy on each row during queries. In order to successfully cache the result across each policy evaluation, we need to trick Postgres.&lt;/p&gt;

&lt;p&gt;When we wrap the function call in a &lt;code&gt;SELECT&lt;/code&gt;, Postgres creates an &lt;code&gt;InitPlan&lt;/code&gt; query node type. By default, anything after the &lt;code&gt;USING&lt;/code&gt; keyword is executed as a &lt;code&gt;SubPlan&lt;/code&gt; type, where Postgres expects that the outcome can change row to row. This is desired as that is what we are checking; for every row, should the user be allowed to fetch it.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;InitPlan&lt;/code&gt; is only run once per execution of the outer plan, and cached for reuse in later rows of the evaluation. Using &lt;code&gt;EXPLAIN&lt;/code&gt;, we can see how the different policy definitions change the estimated cost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- RLS without subquery: no InitPlan, high cost
CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.tenant_id')::bigint AND get_current_role() = 'admin');
EXPLAIN:
    Aggregate  (cost=34828.68..34828.69 rows=1 width=40)
      -&amp;gt;  Index Scan using orders_tenant_id_idx on orders  (cost=0.43..34826.20 rows=495 width=6)
            Index Cond: (tenant_id = (current_setting('app.tenant_id'::text))::bigint)
            Filter: (get_current_role() = 'admin'::text)

-- RLS with subquery: Initplan caches result, lower cost
CREATE POLICY tenant_isolation ON orders USING  (tenant_id = current_setting('app.tenant_id')::bigint AND (SELECT get_current_role()) = 'admin');
EXPLAIN:
    Aggregate  (cost=10095.69..10095.70 rows=1 width=40)
      InitPlan 1
        -&amp;gt;  Result  (cost=0.00..0.26 rows=1 width=32)
      -&amp;gt;  Index Scan using orders_tenant_id_idx on orders  (cost=0.43..10092.95 rows=495 width=6)
            Index Cond: (tenant_id = (current_setting('app.tenant_id'::text))::bigint)
            Filter: ((InitPlan 1).col1 = 'admin'::text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cost=&lt;/code&gt; in the explain rows is Postgres' guess at how expensive a query will be to run, in arbitrary units. The first number is the estimated startup cost; or how expensive it is to do the sorting and filtering of the query before returning rows to the user. The second number is the estimated total cost, including fetching all the rows. The &lt;code&gt;rows=&lt;/code&gt; and &lt;code&gt;width=&lt;/code&gt; are how many expected rows the query will return, and the width of those rows respectively.&lt;/p&gt;

&lt;p&gt;When Postgres doesn't think it can cache the inner query, the cost is over 3x higher than if it would have been able to. In reality, the actual latency difference is much larger than 3x as seen in the chart above.&lt;/p&gt;

&lt;p&gt;When Postgres doesn't cache expensive functions in your policy definitions, RLS becomes expensive overhead. RLS can be just as fast as if you weren't using it at all in some scenarios. The issue is that RLS becomes yet another layer of code that needs to continuously optimized, where small mistakes can cause large performance hits.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's your house: Permission ownership
&lt;/h2&gt;

&lt;p&gt;It's your house, you obviously have the keys to everything, but what if you weren't supposed to?&lt;/p&gt;

&lt;p&gt;Every Postgres table has an owner. Normally you'd control table and row access on a per-Postgres-role basis, however when you connect to Postgres as the owning role of a table, none of its RLS policies apply. You must explicitly opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;FORCE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even this may not be sufficient if you are connected with the Postgres superuser role. Any roles that contain the &lt;code&gt;SUPERUSER&lt;/code&gt; attribute will always bypass RLS. This is easy to miss and easy to test incorrectly. Your policy tests might pass under a non-owner role while production traffic runs as the owner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making a ham sandwich: Stricter patterns
&lt;/h3&gt;

&lt;p&gt;Let's say your friend Andy wanted to make a ham sandwich. He had access to the fridge and utensils, but not your grocery list. When he made his sandwich, he used up all the mustard, and now you need to go get more. When using RLS, Andy's query can't touch our grocery list. We have to update that separately.&lt;/p&gt;

&lt;p&gt;Without RLS this is easy. When using RLS, doing this type of query can add a lot of complexity. Getting the utensils, making the sandwich, and updating the grocery list might not share the same permissions. While rows in one table may be accessible to a user, updating rows in another may not be. Since we own the grocery list, we don't want anyone touching it except in well defined scenarios.&lt;/p&gt;

&lt;p&gt;One way to solve this is by using multiple roles and multiple transactions, but this becomes overly cumbersome on our application layer. A better solution would be to add a &lt;code&gt;SECURITY DEFINER&lt;/code&gt; function in our database that gives roles access to modify or view data in a well defined way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;use_ingredients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingredients&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Runs as the function owner, bypassing Andy's RLS policies&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;grocery_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingredients&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SECURITY DEFINER&lt;/code&gt; causes the function to run as its owner's role, bypassing RLS entirely for that operation. Now you're back to managing security on both RLS and your application layer, ensuring only specific parameters are allowed to pass to this function.&lt;/p&gt;

&lt;p&gt;Keeping database functions in version control also becomes difficult. Some migration tools include SQL functions and policies, but are another part of your schema migrations that can cause headaches down the road.&lt;/p&gt;

&lt;p&gt;Your application layer also needs to stay in sync with every function it calls in your database. Changing function definitions, names, or return values may require a new database migration, or delicate surgery to ensure a stable update.&lt;/p&gt;

&lt;h2&gt;
  
  
  End of the day
&lt;/h2&gt;

&lt;p&gt;Once we have managed locking everything under a different key inside your house, who has what keys, who is allowed in, and who is delegating access for who, we find our application code has almost as much logic as if it didn't have RLS at all.&lt;/p&gt;

&lt;p&gt;RLS policies themselves are stored in &lt;code&gt;pg_policies&lt;/code&gt; inside your database, not in your source code. Most standard migration tools don't track policy changes alongside schema changes. Policy migrations become a separate, manual process, and they drift. A schema change that adds a column or renames a table can silently break a policy that no one realizes is outdated until something breaks in our application, impacting users.&lt;/p&gt;

&lt;p&gt;Each query to the database will already need some sort of modifier in your application code to add local variables for user identification when using PgBouncer. Misconfigured local variables could be just as damaging as if RLS wasn't there to begin with.&lt;/p&gt;

&lt;p&gt;We still need to check early on if a user has permission to run a query, or else we risk allowing users to degrade our database performance with spam. If we are already checking permissions at the application layer, the benefits of RLS become harder to observe.&lt;/p&gt;

&lt;p&gt;Optimizing queries also becomes much harder. Queries are artificially restricted to what they are allowed to see, and need bespoke functions and permissions to get access. This causes our management of source code and database logic to become even harder to manage, between policies, functions, and the mappings between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to do it right
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://pscale.link/7uQCLEy" rel="noopener noreferrer"&gt;PlanetScale&lt;/a&gt;, we typically recommend against relying on Postgres RLS. There may be occasional useful scenarios, but when implementing RLS correctly at scale, the benefits quickly turn into cons with a higher overhead not only to performance, but also developer experience and complexity.&lt;/p&gt;

&lt;p&gt;Application-layer authorization like middleware, ORM-level scoping, or a dedicated permissions table keeps your logic visible, testable, and co-located with the code that uses it.&lt;/p&gt;

&lt;p&gt;Your database is more like a warehouse. Don't treat it like your house.&lt;/p&gt;

&lt;h2&gt;
  
  
  Footnotes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Note that PgBouncer &lt;code&gt;pool_mode&lt;/code&gt; must be in either &lt;code&gt;session&lt;/code&gt; or &lt;code&gt;transaction&lt;/code&gt;. &lt;code&gt;statement&lt;/code&gt; mode won't work with &lt;code&gt;SET LOCAL&lt;/code&gt; at all.&lt;/li&gt;
&lt;li&gt;The Postgres query planner can sometimes determine that a policy is safe to cache across evaluations on its own. Doing this properly can be a tricky process. Even in our benchmark example, functions that are marked as stable still need to be wrapped in a subquery in order for Postgres to properly cache the result. Each policy is different, and determining the proper optimizations for each one is another layer of complexity in your codebase.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>postgres</category>
      <category>planetscale</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>High Memory Usage in Postgres is Good, Actually</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Mon, 04 May 2026 15:56:50 +0000</pubDate>
      <link>https://dev.to/planetscale/high-memory-usage-in-postgres-is-good-actually-1i49</link>
      <guid>https://dev.to/planetscale/high-memory-usage-in-postgres-is-good-actually-1i49</guid>
      <description>&lt;p&gt;&lt;em&gt;By Simeon Griggs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Houseplants often die from over-watering, not neglect. It is easy to project human needs onto them: "If I am thirsty, they must be thirsty too." But many indoor plants actually benefit from drying out between waterings.&lt;/p&gt;

&lt;p&gt;Similarly, your empathy can lead to misinterpreting signals from your database. You don't like feeling overwhelmed, so you don't want your database overwhelmed either.&lt;/p&gt;

&lt;p&gt;But not all usage is created equal, and memory in computers can be uniquely complex to understand.&lt;/p&gt;

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

&lt;p&gt;A look at your PlanetScale dashboard might show memory usage sitting at 80%. That &lt;em&gt;looks&lt;/em&gt; bad, but it could actually be representative of a healthy system.&lt;/p&gt;

&lt;p&gt;To be clear, consistently high CPU usage is a problem. For as long as CPU stays high, queries wait longer, the slowest queries get slower, and you have less headroom for spikes.&lt;/p&gt;

&lt;p&gt;Memory is different. The percentage shown in the cluster diagram on your PlanetScale dashboard is measuring the entire node your database runs on, not just Postgres. When most RAM is in use, it usually means the system is keeping data close to the CPU so it does not have to read from disk as often. Unlike sustained high CPU, high memory usage by itself does not mean performance is degraded or that you are at immediate risk of "running out" of memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Postgres wants your memory
&lt;/h2&gt;

&lt;p&gt;Reading from disk is slower than reading from RAM, even with &lt;a href="https://planetscale.com/docs/metal" rel="noopener noreferrer"&gt;PlanetScale Metal&lt;/a&gt;'s locally attached NVMe drives. Postgres is designed to take advantage of that gap by caching as much data in memory as it can.&lt;/p&gt;

&lt;p&gt;There are two layers of caching at work, and both consume RAM.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shared_buffers&lt;/code&gt; is Postgres' own buffer pool. When a query needs data, Postgres first checks this pool for the relevant pages, the fixed-size (8 KB by default) chunks of table and index data it works with, before reading from disk. The more of your working data that fits here, the fewer disk reads Postgres needs to perform.&lt;/p&gt;

&lt;p&gt;This parameter can be configured in the &lt;a href="https://planetscale.com/docs/postgres/cluster-configuration/parameters" rel="noopener noreferrer"&gt;cluster configuration&lt;/a&gt; page of the PlanetScale dashboard. The default value should be sufficient for most workloads, and modifying it should not be your first step in troubleshooting memory usage.&lt;/p&gt;

&lt;p&gt;The OS page cache is the second caching layer. Even when Postgres does go to disk, the operating system keeps a copy of the data it reads in RAM so the next access is faster. This is not a Postgres feature — it is standard Linux behavior. Postgres was designed with this in mind, and its own documentation notes that the operating system's cache is expected to handle data beyond what fits in &lt;code&gt;shared_buffers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Between these two layers, a healthy Postgres server will use most of the available RAM. That is the goal, not a side effect. For context, reading a page from RAM is roughly 1,000 times faster than reading it from even a fast NVMe drive. A database that keeps frequently accessed data in memory avoids that penalty on every query.&lt;/p&gt;

&lt;p&gt;When caching is working well, the vast majority of page reads are served from memory without touching disk. If that ratio drops — because the working dataset has outgrown available memory, for example — queries slow down as Postgres waits on disk more often.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See our documentation on &lt;a href="https://planetscale.com/docs/postgres/monitoring/metrics#interpreting-metrics" rel="noopener noreferrer"&gt;"Normal operating ranges"&lt;/a&gt; to sense-check what values you should be seeing in Cluster Metrics for CPU, memory, and more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Memory usage compared to CPU usage
&lt;/h2&gt;

&lt;p&gt;At a glance, CPU and memory usage numbers look comparable because they share a 0–100% scale, but they describe very different behavior.&lt;/p&gt;

&lt;p&gt;CPU is work. Sustained high CPU means the database is spending time on work it cannot skip. When CPU is saturated, queries arrive faster than they can be processed. They queue, latency climbs, and connection timeouts can cascade into application-level failures. There is no "good" kind of sustained high CPU usage.&lt;/p&gt;

&lt;p&gt;Memory is &lt;em&gt;workspace&lt;/em&gt;. Postgres and the OS use spare RAM to avoid expensive disk reads. Higher use improves performance ... most of the time.&lt;/p&gt;

&lt;p&gt;"Most of the time" because memory usage gets a little complicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of memory usage
&lt;/h2&gt;

&lt;p&gt;The single “memory usage” percentage number combines two different behaviors.&lt;/p&gt;

&lt;p&gt;To explore that number in more detail, within the &lt;a href="https://planetscale.com/docs/postgres/monitoring/metrics" rel="noopener noreferrer"&gt;Cluster Metrics&lt;/a&gt; page of the PlanetScale dashboard, memory is shown as a stacked chart over time with four different categories: active cache, inactive cache, RSS, and memory mapped. These four categories can be grouped into two separate but equally important use-cases: cache and process memory.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Cache (active, inactive, and memory mapped)
&lt;/h3&gt;

&lt;p&gt;Much of what looks like “used” memory on a healthy database host is cache: file data the operating system keeps in RAM after reads so the next access is cheap. You may see this referred to as "page cache" in other dashboards.&lt;/p&gt;

&lt;p&gt;Active cache is data the OS recently touched and wants to keep around. Inactive cache hasn't been accessed lately. Memory-mapped pages are cached pages that are backed by real files on disk.&lt;/p&gt;

&lt;p&gt;All three of these cache types are reclaimable by the operating system and can be dropped when something else needs RAM.&lt;/p&gt;

&lt;p&gt;If total memory is high because cache is high, good! Frequently accessed data stays near the CPU for faster access.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Process memory (RSS)
&lt;/h3&gt;

&lt;p&gt;Separately, Postgres holds memory for processes that are actually using it. You will see this referred to as RSS (Resident Set Size) in the PlanetScale dashboard.&lt;/p&gt;

&lt;p&gt;This memory is not reclaimable by the operating system and is what increases &lt;a href="https://planetscale.com/docs/postgres/troubleshooting/out-of-memory" rel="noopener noreferrer"&gt;out of memory (OOM) risk&lt;/a&gt;. High memory usage through high RSS leads to restarts and degraded behavior.&lt;/p&gt;

&lt;p&gt;If total memory is high because RSS is high, that is referred to as memory pressure and is a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Resident Set Size?
&lt;/h3&gt;

&lt;p&gt;Roughly, RSS is the amount of private memory allocated to a process such as stack, heap, catalog/relcache caches, query execution memory like sorts and hash tables.&lt;/p&gt;

&lt;p&gt;Given Postgres' process-per-connection architecture, each process requires some baseline amount of memory. Not every process will consume the same amount of memory.&lt;/p&gt;

&lt;p&gt;Further, some memory use is shared across processes. So calculating RSS use is not as simple as adding up the memory usage of every process.&lt;/p&gt;

&lt;p&gt;RSS increases for a number of reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres may grant multiple &lt;code&gt;work_mem&lt;/code&gt; allocations within a single query; see below for more details.&lt;/li&gt;
&lt;li&gt;Catalog bloat can spike RSS usage, common in multi-tenant schemas using a table-per-tenant pattern.&lt;/li&gt;
&lt;li&gt;The operating system's memory allocator may not return memory efficiently.&lt;/li&gt;
&lt;li&gt;Misbehaving or misconfigured extensions can increase RSS usage.&lt;/li&gt;
&lt;li&gt;Cached plans and prepared statements accumulate per-session memory that is not released until the session ends or the statement is explicitly deallocated.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;em&gt;The &lt;code&gt;work_mem&lt;/code&gt; parameter's default value is set relative to the amount of memory in your database cluster. It can be modified in the &lt;a href="https://planetscale.com/docs/postgres/cluster-configuration/parameters" rel="noopener noreferrer"&gt;cluster configuration&lt;/a&gt; page of the PlanetScale dashboard.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tuning &lt;code&gt;work_mem&lt;/code&gt; might seem like an obvious lever — decrease it to reduce RSS, or increase it to prevent operations from spilling to disk. But the allocation is per-sort/hash-node, per-query, per-backend.&lt;/p&gt;

&lt;p&gt;A single complex query can allocate &lt;code&gt;work_mem&lt;/code&gt; multiple times, and that multiplies across every active connection. Setting it too low forces more disk I/O; setting it too high globally can cause total memory usage to spike unpredictably under load. Neither direction is a safe default change without first understanding your workload's concurrency and query complexity.&lt;/p&gt;

&lt;p&gt;Efficient connection pooling can be the best way to reduce RSS usage. Fewer active connections result in fewer copies of all that per-process overhead.&lt;/p&gt;

&lt;p&gt;PgBouncer on PlanetScale runs in transaction mode, where connections are returned to the pool after each transaction completes. See our blog post on &lt;a href="https://planetscale.com/blog/scaling-postgres-connections-with-pgbouncer" rel="noopener noreferrer"&gt;Scaling Postgres connections with PgBouncer&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigating memory usage while debugging performance
&lt;/h2&gt;

&lt;p&gt;If you're experiencing degraded performance, the challenge is figuring out what drove the RSS growth.&lt;/p&gt;

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

&lt;p&gt;Query Insights helps you investigate query performance through CPU time, I/O, and latency, but it does not show per-query memory. You may see OOM markers and slow-query signals, but not query-specific RSS usage.&lt;/p&gt;

&lt;p&gt;RSS is a per-process metric, not a per-query metric. That means you cannot read “RSS per query” directly from &lt;code&gt;EXPLAIN&lt;/code&gt; or Query Insights. Instead, you may need to gather multiple signals and triangulate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use Cluster Metrics to identify when RSS rises.&lt;/li&gt;
&lt;li&gt;In Query Insights for that same window, look for expensive patterns (high runtime, CPU, I/O, rows/blocks read) and OOM-adjacent activity.&lt;/li&gt;
&lt;li&gt;Re-run suspect queries with &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, MEMORY)&lt;/code&gt; to inspect operator-level memory usage.&lt;/li&gt;
&lt;li&gt;Check connection counts in the same window, because many concurrent connection processes can increase RSS even when a single query is moderate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://planetscale.com/docs/postgres/troubleshooting/out-of-memory" rel="noopener noreferrer"&gt;out of memory&lt;/a&gt; documentation has more details on the likely causes of, and how to prevent, OOM events.&lt;/p&gt;

&lt;h2&gt;
  
  
  In summary
&lt;/h2&gt;

&lt;p&gt;A lot of cached data in memory is a good thing. Ideally, your "hot dataset" fits in the page cache of your database cluster to maintain fast performance. Too little cached data can lead to increased CPU usage and degraded performance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High memory usage is not automatically bad.&lt;/strong&gt; If your high memory usage is due to cache, you typically have a healthy, performant database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory pressure is bad.&lt;/strong&gt; Rising RSS toward limits, OOM kills, unexplained restarts, and tail latency spiking together with heavy disk I/O when the working set is tight on RAM are the signals to act on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustained high CPU is a problem.&lt;/strong&gt; It means you are out of headroom. Tune the workload (see &lt;a href="https://planetscale.com/docs/postgres/monitoring/query-insights" rel="noopener noreferrer"&gt;Query Insights&lt;/a&gt;) or upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the dashboard shows a high “% memory used,” do not panic. Investigate the types of memory being used and check for OOM events before taking action.&lt;/p&gt;

</description>
      <category>planetscale</category>
      <category>postgres</category>
      <category>database</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stripe Projects Partnership: Provision PlanetScale Postgres and MySQL Databases From the Stripe CLI</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Mon, 27 Apr 2026 17:55:54 +0000</pubDate>
      <link>https://dev.to/planetscale/stripe-projects-partnership-provision-planetscale-postgres-and-mysql-databases-from-the-stripe-cli-2380</link>
      <guid>https://dev.to/planetscale/stripe-projects-partnership-provision-planetscale-postgres-and-mysql-databases-from-the-stripe-cli-2380</guid>
      <description>&lt;p&gt;&lt;em&gt;By Elom Gomez&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Did you hear the news? PlanetScale is participating as a co-design and launch partner for Stripe Projects, a new developer preview from Stripe that centralizes dev tool provisioning and billing in one place.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/qU4lHe-2iRQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Stripe Projects?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.stripe.com/projects" rel="noopener noreferrer"&gt;Stripe Projects&lt;/a&gt; is a new way for developers and coding agents to discover, provision, and pay for developer tools all from the Stripe CLI. Instead of jumping between dashboards, entering payment info, and copying credentials across services, everything lives in one centralized workflow.&lt;/p&gt;

&lt;p&gt;This fragmented developer workflow has always existed, but AI agents have made the gap much more obvious. The ecosystem has been missing a standard way for provisioning and credential handoff to work reliably across providers. And we're excited to partner with Stripe to close this gap.&lt;/p&gt;

&lt;p&gt;With PlanetScale as a launch partner, you can now spin up and pay for fully managed MySQL or Postgres databases directly from your terminal in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out today
&lt;/h2&gt;

&lt;p&gt;Stripe Projects is currently in developer preview. You can request early access &lt;a href="https://projects.dev/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Once you're in, follow these instructions to spin up a PlanetScale Postgres or MySQL database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://docs.stripe.com/stripe-cli" rel="noopener noreferrer"&gt;Stripe CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install the Projects plugin: &lt;code&gt;stripe plugin install projects&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Initialize Stripe Projects in your app &lt;code&gt;stripe projects init&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a PlanetScale database: &lt;code&gt;stripe projects add planetscale/postgresql&lt;/code&gt; or &lt;code&gt;stripe projects add planetscale/mysql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go through the prompts to create your database: database name, cluster size, region, and number of replicas&lt;/li&gt;
&lt;li&gt;Within seconds, your PlanetScale Postgres or MySQL database is provisioned without you ever leaving the terminal&lt;/li&gt;
&lt;li&gt;Sync your database credentials to your &lt;code&gt;.env&lt;/code&gt; file: &lt;code&gt;stripe projects env --sync&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Resources and feedback
&lt;/h2&gt;

&lt;p&gt;You can start using PlanetScale with Stripe Projects in the &lt;a href="https://marketplace.stripe.com/apps/planetscale" rel="noopener noreferrer"&gt;Stripe Marketplace&lt;/a&gt;. Or, head to the &lt;a href="https://docs.stripe.com/projects" rel="noopener noreferrer"&gt;Stripe Projects documentation&lt;/a&gt; to learn more.&lt;/p&gt;

&lt;p&gt;We'd love to hear how you're using PlanetScale with Stripe Projects. &lt;a href="https://pscale.link/community" rel="noopener noreferrer"&gt;Join our Discord&lt;/a&gt; to let us know or &lt;a href="https://x.com/PlanetScale" rel="noopener noreferrer"&gt;reach out to us on X&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>planetscale</category>
      <category>postgres</category>
      <category>mysql</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Enhanced Tagging in Postgres Query Insights</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Mon, 20 Apr 2026 16:14:33 +0000</pubDate>
      <link>https://dev.to/planetscale/enhanced-tagging-in-postgres-query-insights-5ae0</link>
      <guid>https://dev.to/planetscale/enhanced-tagging-in-postgres-query-insights-5ae0</guid>
      <description>&lt;p&gt;&lt;em&gt;By Rafer Hazen&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As part of our &lt;a href="https://planetscale.com/blog/introducing-database-traffic-control" rel="noopener noreferrer"&gt;Traffic Control launch&lt;/a&gt;, we made enhancements to the Insights query tagging feature for Postgres databases. Insights has supported query tags for some time, but they were previously only attached as metadata on individual notable query logs. With this release, tags are now present in aggregated query data, which enables powerful new capabilities. It's now possible to view the complete distribution of tags assigned to a query pattern, search queries by tag, and see a per-tag breakdown of database-level statistics. This blog post gives an overview of the feature, and digs into the details of how we implemented it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Tags
&lt;/h2&gt;

&lt;p&gt;Query tags are string key-value pairs that are included in query SQL using specially formatted &lt;a href="https://google.github.io/sqlcommenter/" rel="noopener noreferrer"&gt;SQL comments&lt;/a&gt;. For example, the following query has the &lt;code&gt;controller&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; tags attached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; 
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
  &lt;span class="cm"&gt;/* controller='users',
     action='show' */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typically tags are specified at the application level and applied automatically to all queries issued by the database framework you're using. Common examples are &lt;code&gt;controller&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;job&lt;/code&gt;, or &lt;code&gt;source_location&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In addition to tags set by the database client, Insights automatically adds the following tags to all queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;application_name&lt;/code&gt; - set by the Postgres driver&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;username&lt;/code&gt; - the Postgres user executing the query&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remote_address&lt;/code&gt; - the remote IP address&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feature Overview
&lt;/h2&gt;

&lt;p&gt;This feature introduces three new surfaces where tag information can be seen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Pattern Tags
&lt;/h3&gt;

&lt;p&gt;To see the set of tags associated with a given query pattern, click on a query pattern from the main Insights dashboard. This page lists the tags that have been submitted with a given query pattern over a particular time range, as well as the percentage of queries that included each tag value.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Database Tags
&lt;/h3&gt;

&lt;p&gt;To see aggregate statistics for your entire database broken down by tag, go to the Tags section in the Insights sidebar and select the tag or set of tags that you want to view.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Query Filter
&lt;/h3&gt;

&lt;p&gt;To see a list of query patterns that have a given tag value, go to the Insights dashboard and search for a particular tag with &lt;code&gt;tag:MY_TAG:MY_VALUE&lt;/code&gt;. The returned query patterns and statistics are filtered to only queries with the specified tag pair.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;To understand how tagging works in Insights, it helps to understand the underlying data sources that power Insights. Query performance data is observed by the Insights Postgres extension, emitted to Kafka and written to ClickHouse. The extension publishes to two separate Kafka topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Individual queries - any query reading more than 10,000 rows, taking longer than 1 second, or resulting in an error. One message is sent per qualifying query. This powers the &lt;a href="https://planetscale.com/docs/postgres/monitoring/query-insights#notable-queries" rel="noopener noreferrer"&gt;Notable queries&lt;/a&gt; feature.&lt;/li&gt;
&lt;li&gt;Aggregate summaries - &lt;a href="https://planetscale.com/docs/postgres/monitoring/query-insights#available-query-statistics" rel="noopener noreferrer"&gt;statistics&lt;/a&gt; like total query count, rows read, and cumulative query time. One message is sent for every &lt;a href="https://planetscale.com/blog/query-performance-analysis-with-insights#query-patterns" rel="noopener noreferrer"&gt;query pattern&lt;/a&gt; every 15 seconds. This powers the majority of Insights including the query table, anomalies, and all query-related graphs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prior to this release, tag data was only attached to the individual query data stream. This adds important information to notable queries, but because the data wasn't present in the aggregate summaries, it wasn't possible to filter or group aggregate data by tag. Insights couldn't answer important questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What queries has this user executed?&lt;/li&gt;
&lt;li&gt;What percentage of my total query run time is coming from this controller?&lt;/li&gt;
&lt;li&gt;Which background jobs are executing this query?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our goal with this release was to associate all query data with the relevant tags to make it possible to answer this class of questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending Tags
&lt;/h3&gt;

&lt;p&gt;To explore the various approaches for implementing tags, let's use the following query executions as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="cm"&gt;/*controller='users'*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="cm"&gt;/*controller='sessions'*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="cm"&gt;/*controller='sessions'*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since each of these queries has the same fingerprint (query with all literal values removed), without tags we would only need to send a single summary message. To include tags, we have several options. The first would be to continue sending only a single query summary event with a count of how many times each tag was observed. This would produce a summary message like the following (other stats fields are omitted):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;sql:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"select * from users where id = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;query_count:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;total_time:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"100ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;tags:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"controller=users"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"controller=sessions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This message tells us the given query was executed three times - twice from the sessions controller and once from the users controller - and had a cumulative execution time of 100ms.&lt;/p&gt;

&lt;p&gt;At first glance, including tags in this manner is an attractive option. It's simple to implement - we just accumulate tags along with the other aggregate stats - and it doesn't increase the number of events that need to be emitted and stored. It has a serious shortcoming, however: it's not possible to attribute aggregated stats to any individual tag. For example, it's not possible to know the total time of queries emitted from just the users controller, because we can't tell what portion of the 100ms was associated with &lt;code&gt;controller=users&lt;/code&gt;. The summary data for one tag is permanently combined with the data from all tags.&lt;/p&gt;

&lt;p&gt;To overcome this limitation, we can instead emit a separate aggregate summary message for each set of unique tags. In our example this would mean we emit two separate messages to the insights pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;sql:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"select * from users where id = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;query_count:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;total_time:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;tags:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"controller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;sql:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"select * from users where id = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;query_count:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;total_time:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"80ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;tags:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"controller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sessions"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach makes it possible to fully disambiguate aggregated statistics based on the attached tags. We can tell that the users controller was responsible for exactly 20ms of total execution time and the sessions controller was responsible for exactly 80ms.&lt;/p&gt;

&lt;p&gt;This comes at a cost though: we have to emit a separate message for each unique tag combination. This can be problematic for high-cardinality tags (tags with a large number of distinct values). Consider a customer that has set a &lt;code&gt;request_id&lt;/code&gt; tag on all of the queries issued from their web tier. Where we previously would be able to collapse 500 user-lookup queries into a single summary message, we now have to send 500 messages because they each have a unique &lt;code&gt;request_id&lt;/code&gt;. In the worst case, this means that the summary data stream must send one summary message &lt;em&gt;per query execution&lt;/em&gt;, and we've lost all of the scalability advantages of aggregating query statistics. For large clusters executing millions of queries per second, this would be prohibitively expensive to process and store, and would consume considerable resources on the database host where telemetry data is emitted.&lt;/p&gt;

&lt;p&gt;To prevent this from overwhelming the pipeline, we implemented several strategies to dynamically reduce the cardinality of tags and therefore decrease the number of messages that must be handled by the Insights pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cardinality Reduction
&lt;/h2&gt;

&lt;p&gt;The core idea is simple: when a tag (or set of tags) would result in sending too much telemetry data, we collapse that tag by replacing specific values (like &lt;code&gt;request_id="a"&lt;/code&gt; and &lt;code&gt;request_id="b"&lt;/code&gt;) with a value that indicates it has been removed: &lt;code&gt;request_id=*&lt;/code&gt;. This lets us more aggressively merge aggregates and reduce the total number of messages sent, while ensuring that we're capturing 100% of the summary data.&lt;/p&gt;

&lt;p&gt;We employed two separate approaches for tag collapsing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-tag Limits
&lt;/h3&gt;

&lt;p&gt;This mechanism tracks the number of unique values seen for each tag key, scoped per query pattern. If that count exceeds a predefined limit (currently 20), we proactively collapse that key for all queries for the next hour. This catches inherently high-cardinality tags like &lt;code&gt;request_id&lt;/code&gt; or &lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An important part of this approach is that cardinality is monitored &lt;em&gt;per query pattern&lt;/em&gt; and not globally. Consider the &lt;code&gt;source_location&lt;/code&gt; tag that contains the file and line number showing where the query was initiated in the client app. Overall this tag is high-cardinality, because each query pattern likely has its own unique value for &lt;code&gt;source_location&lt;/code&gt;, but it is highly correlated with the query pattern so it doesn't actually result in additional messages being sent to the pipeline - we are already sending a separate query summary message for each query pattern. Monitoring cardinality per-pattern allows high-cardinality tags that are highly correlated with query pattern to pass through without being collapsed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-interval Limits
&lt;/h3&gt;

&lt;p&gt;Within each 15-second interval, we track all aggregates keyed by their unique set of tag key-value pairs. Because we must emit a message for each unique &lt;em&gt;combination&lt;/em&gt; of tags, even individually low-cardinality tags could produce an unacceptably large number of &lt;em&gt;combinations&lt;/em&gt; of tags. For example, if a query pattern has 6 tag keys that each have 10 distinct values, there could be 10^6 individual tag combinations. To prevent an explosion in the number of messages that must be tracked, we perform dynamic cardinality reduction on a per-interval basis for any individual query pattern that has more than a fixed number of tag combinations.&lt;/p&gt;

&lt;p&gt;To reduce the combined cardinality of a given set of aggregates, we find the highest cardinality tag and collapse it (replace all values with a single value). We successively perform this operation until the number of aggregates is beneath the fixed threshold (currently set to 50 in production).&lt;/p&gt;

&lt;p&gt;To illustrate this operation, consider five executions of the same query pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="cm"&gt;/*controller='users',    host='app-1'*/&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="cm"&gt;/*controller='users',    host='app-2'*/&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="cm"&gt;/*controller='sessions', host='app-3'*/&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="cm"&gt;/*controller='sessions', host='app-4'*/&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="cm"&gt;/*controller='sessions', host='app-1'*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without any limits, this produces five separate aggregate messages. To reduce the aggregate message count, we identify that the host tag has the highest cardinality (4 unique values) and replace all of its values with a placeholder and merge the remaining results. This yields only two combinations that must be emitted to the pipeline, one for each of the two unique &lt;code&gt;controller&lt;/code&gt; tag values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracking Tag Collapsing
&lt;/h3&gt;

&lt;p&gt;When a tag must be collapsed due to either of the cardinality limitation mechanisms, we record the fact that the key has been collapsed in the emitted aggregate message. This allows us to detect when collapsing has occurred and display a message noting the percentage of tag values where the value is unknown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Query tagging is a powerful feature. Being able to slice your Insights data by arbitrary tags gives you a much clearer picture of your database performance. We're excited for you to try it.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>planetscale</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Patterns for Postgres Traffic Control</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Mon, 13 Apr 2026 16:44:20 +0000</pubDate>
      <link>https://dev.to/planetscale/patterns-for-postgres-traffic-control-2mlo</link>
      <guid>https://dev.to/planetscale/patterns-for-postgres-traffic-control-2mlo</guid>
      <description>&lt;p&gt;&lt;em&gt;By Josh Brown&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last month we introduced &lt;a href="https://planetscale.com/blog/introducing-database-traffic-control" rel="noopener noreferrer"&gt;Database Traffic Control™&lt;/a&gt;. Traffic Control lets you attach resource budgets to slices of your Postgres traffic, like keeping your checkout flow running while a runaway analytics query gets shed instead. We have already discussed &lt;a href="https://planetscale.com/docs/postgres/traffic-control/examples-and-recipes" rel="noopener noreferrer"&gt;some scenarios where&lt;/a&gt; you should use Traffic Control, along with &lt;a href="https://planetscale.com/blog/graceful-degradation-in-postgres" rel="noopener noreferrer"&gt;how to define resource limits&lt;/a&gt;, so now let's dig into what Traffic Control looks like in your codebase.&lt;/p&gt;

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

&lt;p&gt;This post walks through some practical patterns in Go. Each pattern targets a different failure mode, architecture, or foot gun. Most of them layer on top of one another too, so you can adopt them individually or combine them for extra peace of mind. Keep in mind the general concepts here are applicable to whatever language your application is written in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Most of the patterns here rely on custom tags attached to your queries. Traffic Control reads these using the &lt;a href="https://google.github.io/sqlcommenter/" rel="noopener noreferrer"&gt;SQLCommenter&lt;/a&gt; format: a SQL comment appended to each query with URL-encoded key=value pairs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="cm"&gt;/*route='checkout',feature='new_order_flow'*/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tags are then available for new Traffic Control rules.&lt;/p&gt;

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

&lt;p&gt;Here's a minimal Go helper that appends tags in this format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"net/url"&lt;/span&gt;
    &lt;span class="s"&gt;"sort"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// appendTags appends SQLCommenter-format tags to a SQL query.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;appendTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s='%s'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryEscape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// deterministic order&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" /*"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"*/"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll also want a way to thread tags through your call stack without touching every function signature. A context key works well for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;contextKey&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sqlTagsKey&lt;/span&gt; &lt;span class="n"&gt;contextKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sql_tags"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlTagsKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// return a copy so callers can't mutate shared state&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;contextWithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlTagsKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these two helpers in place, the patterns below mostly just set keys and values in context. Tagging happens automatically when the query executes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-service isolation via Roles
&lt;/h2&gt;

&lt;p&gt;In a microservice architecture, a single misbehaving service should not be able to degrade every other service sharing the same database. The simplest way to isolate a service is to create a Traffic Control rule based on a unique connection string for the given service, or via application name.&lt;/p&gt;

&lt;p&gt;A budget on &lt;code&gt;username='pscale_api_123abc'&lt;/code&gt; will isolate all traffic from that role. This also helps in incident response: you can immediately cap a service's resource share without redeploying anything.&lt;/p&gt;

&lt;p&gt;Note that the username is the internal Postgres username of the role, not the dashboard role name. You can also target custom roles created by CREATE ROLE if your microservices have strict security over table permissions.&lt;/p&gt;

&lt;p&gt;You can also use the &lt;code&gt;application_name&lt;/code&gt; by appending it to your connection strings such as &lt;code&gt;postgresql://other@localhost/otherdb?application_name=myapp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route-level tagging in an HTTP service
&lt;/h2&gt;

&lt;p&gt;When you're running a monolith or a large API service, the problem isn't usually the whole service, it's specific routes. The &lt;code&gt;/api/export&lt;/code&gt; endpoint that generates CSV reports should not be able to kill the &lt;code&gt;/api/checkout&lt;/code&gt; flow.&lt;/p&gt;

&lt;p&gt;An HTTP middleware can inject the route into context at runtime before any handler runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Any route using SQLTagMiddleware will have the pattern injected into its context&lt;/span&gt;
&lt;span class="c"&gt;// dynamically at runtime&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;SQLTagMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReplaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReplaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Removes "{}" characters from route&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"web"&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;contextWithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap your database calls to pick up the tags automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// QueryContext for SELECT statements&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appendTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ExecContext for INSERT/UPDATE statements&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appendTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every query carries the route it came from. You can create a Traffic Control budget targeting &lt;code&gt;route='/api-export'&lt;/code&gt; and give it a conservative CPU limit.&lt;/p&gt;

&lt;p&gt;This also makes it easy to set up broad budgets during incidents. If you suddenly see a spike and don't know which route is responsible, the violation graph in Traffic Control will show you exactly which route tag is hitting limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature flags and new deployments
&lt;/h2&gt;

&lt;p&gt;Shipping a new feature to production always carries risk. Maybe the new query pattern is fine under your test load but becomes expensive at scale. Traffic Control gives you a way to cap the blast radius before it becomes an incident.&lt;/p&gt;

&lt;p&gt;The simplest version sets a tag from an environment variable at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deploymentTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEPLOYMENT_TAG"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// e.g. "new_checkout_v2" or git sha "96e350426"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;tagWithDeployment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;deploymentTag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"feature"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deploymentTag&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;contextWithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;DEPLOYMENT_TAG=new_checkout_v2&lt;/code&gt; when rolling out new pods and leave it unset on the old pods. Traffic Control can then have a budget on &lt;code&gt;feature='new_checkout_v2'&lt;/code&gt; in Warn mode from day one, so you see exactly how the new code behaves before it causes problems. When you're confident, either remove the budget or switch it to Enforce as a safety net.&lt;/p&gt;

&lt;p&gt;For feature flags controlled at runtime, the same approach works but driven by your flag evaluation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"new_order_flow"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"feature"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"new_order_flow"&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextWithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tier-based limits in multi-tenant apps
&lt;/h3&gt;

&lt;p&gt;In a SaaS application, free-tier users should not be able to degrade the experience for enterprise customers. Traffic Control lets you enforce this at the database level rather than just at the application layer.&lt;/p&gt;

&lt;p&gt;Inject the user's subscription tier into the SQL tags early in your request handling — ideally right after you've resolved the authenticated user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Tier&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TierFree&lt;/span&gt;       &lt;span class="n"&gt;Tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"FREE"&lt;/span&gt;
    &lt;span class="n"&gt;TierPro&lt;/span&gt;        &lt;span class="n"&gt;Tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PRO"&lt;/span&gt;
    &lt;span class="n"&gt;TierEnterprise&lt;/span&gt; &lt;span class="n"&gt;Tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ENTERPRISE"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WithUserTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="n"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tagsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;contextWithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your authentication middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;AuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnauthorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;WithUserTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, create two Traffic Control budgets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tier='free'&lt;/code&gt; — conservative limits on server share and max concurrent queries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tier='pro'&lt;/code&gt; — moderate limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave enterprise traffic unbudgeted or give it a high budget as a ceiling. When a free-tier user runs an expensive dashboard or triggers a slow query, the budget sheds that traffic before it touches enterprise workloads.&lt;/p&gt;

&lt;p&gt;You can combine this with the route tag from Pattern 2. A budget matching &lt;code&gt;tier='free' AND route='api-export'&lt;/code&gt; can be stricter than a budget on &lt;code&gt;tier='free'&lt;/code&gt; alone. Enterprise export requests get more headroom than free-tier export requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background jobs and scripts
&lt;/h2&gt;

&lt;p&gt;Background jobs are a common cause of database incidents. A migration script, a nightly sync, or a one-off data backfill can all accidentally saturate your database if they run faster than expected. Traffic Control is a clean way to give these jobs a resource ceiling without having to tune query-level timeouts throughout your codebase.&lt;/p&gt;

&lt;p&gt;For long-running background workers, use a dedicated connection pool with a distinct &lt;code&gt;application_name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newJobDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;jobDSN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// your connection string&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;jobDSN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// This sets the application name in code instead of in the connection string env variable.&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"background-jobs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jobDSN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pgx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobDSN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// connects to Postgres&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetMaxOpenConns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Jobs don't need high concurrency&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;newJobDB&lt;/code&gt; takes the DSN of your database and sets &lt;code&gt;application_name&lt;/code&gt; to &lt;code&gt;background-jobs&lt;/code&gt; before connecting. Once connected we set the max connections to 4 to make sure our background job isn't taking up more workers than it should, and finally we return it so that the calling function can now query the database.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;application_name&lt;/code&gt; on the connection string level in code ensures that it is always set for this service, no matter the query or connection string given. You can pair this with SQL comments as described above for even more fine-grained control and insights into your queries.&lt;/p&gt;

&lt;p&gt;For one-off scripts and migrations we can do something similar. Here we encode the script's identity directly in the connection string so it shows up clearly in Traffic Control and Insights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Returns a database instance with the `application_name` set to `script-[scriptName]`&lt;/span&gt;
&lt;span class="c"&gt;// for use in scripts&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;scriptDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scriptName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"script-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;scriptName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// e.g. "script-backfill-order-totals"&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pgx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Traffic Control budget for &lt;code&gt;application_name='background-jobs'&lt;/code&gt; in Warn mode before you run this job next. Observe how much of the database's resources your background work typically consumes. Then switch to Enforce to cap it at a level where it can't crowd out interactive traffic even if a job goes sideways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling blocked queries
&lt;/h2&gt;

&lt;p&gt;When Traffic Control is in Enforce mode and a query exceeds its budget, Postgres returns SQLSTATE &lt;code&gt;53000&lt;/code&gt; with an error message prefixed with &lt;code&gt;[PGINSIGHTS] Traffic Control:&lt;/code&gt;. Your application needs to handle this without crashing.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;pgx/v5&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/jackc/pgx/v5/pgconn"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sqlstateTrafficControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"53000"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;isTrafficControlError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pgErr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgconn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PgError&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;As&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pgErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;pgErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sqlstateTrafficControl&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right response depends on the query's role in your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUserOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`SELECT id, total FROM orders WHERE user_id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isTrafficControlError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// Return a degraded response rather than a 500&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrServiceUnavailable&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For non-critical workloads like analytics or reporting, returning a &lt;code&gt;503 Service Unavailable&lt;/code&gt; or a cached result is most likely the right behavior. That's exactly the controlled failure mode Traffic Control is designed to create. For more critical paths, you may want a short retry with backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;queryWithBackoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isTrafficControlError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"overloaded"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observing warn-mode notices
&lt;/h3&gt;

&lt;p&gt;Before switching a budget to Enforce, you'll run it in Warn mode. In Warn mode, queries succeed but the driver receives a Postgres notice containing &lt;code&gt;[PGINSIGHTS] Traffic Control:&lt;/code&gt;. With &lt;code&gt;pgx/v5&lt;/code&gt; you can log these notices to build an accurate picture of what would be blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/jackc/pgx/v5"&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pgx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnNotice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgconn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PgConn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgconn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"[PGINSIGHTS] Traffic Control:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"traffic control warning: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c"&gt;// Increment a metric, write to a structured log, etc.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collect these logs for a few hours of representative traffic before switching to Enforce. The pattern of which rules fire and how often tells you whether your limits need adjustment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting it together
&lt;/h3&gt;

&lt;p&gt;These patterns compose. A real application might layer several of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;setupMiddleware&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// register routes...&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SQLTagMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// Pattern 2: route tags&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Pattern 4: tier tags&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// At startup, the job worker uses Pattern 5: Background jobs&lt;/span&gt;
&lt;span class="n"&gt;jobDB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newJobDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// New features use Pattern 3:&lt;/span&gt;
&lt;span class="c"&gt;// DEPLOYMENT_TAG=new_checkout_v2 set in the deployment manifest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traffic Control sees all of this as a combination of tags. A budget on &lt;code&gt;tier='free'&lt;/code&gt; covers all free-tier traffic regardless of route. A budget on &lt;code&gt;route='api-export' AND tier='free'&lt;/code&gt; covers a specific combination. Multiple matching budgets all apply simultaneously and queries must satisfy every budget they match. You can build layered policies without complicated rule logic.&lt;/p&gt;

&lt;p&gt;Start in Warn mode, observe which budgets would fire during normal load, tighten the limits until only pathological cases trigger violations, then switch to Enforce. The &lt;a href="https://planetscale.com/docs/postgres/traffic-control/getting-started" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt; walks through this rollout process in detail.&lt;/p&gt;

&lt;p&gt;The difference between a database outage and a degraded experience often comes down to whether you've decided in advance which traffic to shed. Traffic Control makes that decision explicit and configurable instead of leaving it to whichever query happens to win a resource race.&lt;/p&gt;

</description>
      <category>planetscale</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Scaling Postgres Connections With PgBouncer</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Mon, 06 Apr 2026 16:59:32 +0000</pubDate>
      <link>https://dev.to/planetscale/scaling-postgres-connections-with-pgbouncer-aff</link>
      <guid>https://dev.to/planetscale/scaling-postgres-connections-with-pgbouncer-aff</guid>
      <description>&lt;p&gt;&lt;em&gt;By Ben Dicken&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Postgres process-per-connection architecture has an elegant simplicity, but hinders performance when tons of clients need to connect simultaneously.&lt;/p&gt;

&lt;p&gt;The near-universal choice for solving this problem is PgBouncer. Though there are upcoming systems like Neki which will solve this problem in a more robust way, PgBouncer has proven itself an excellent connection pooler for Postgres.&lt;/p&gt;

&lt;p&gt;PlanetScale gives you local PgBouncers by default, and makes it incredibly easy to add dedicated ones when needed. The challenge comes in determining the optimal configuration for your app, which is highly use-case dependent.&lt;/p&gt;

&lt;p&gt;My aim with this article is to make every engineer well-equipped to tune PgBouncer with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PgBouncer?
&lt;/h2&gt;

&lt;p&gt;PgBouncer is a lightweight connection pooler that sits between your application and Postgres.&lt;/p&gt;

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

&lt;p&gt;PgBouncer is totally transparent, speaking the PostgreSQL wire protocol. From an app's perspective, it's just talking to a Postgres server, PgBouncer acting as a lightweight middleman. It can multiplex thousands of client connections onto tens of Postgres connections.&lt;/p&gt;

&lt;p&gt;But why not just make 1000s of connections directly to Postgres? Unfortunately, the Postgres process-per-connection architecture doesn't scale well. Every connection forks a dedicated OS process consuming 5+ MB of RAM and adding context-switching overhead. PgBouncer solves this by maintaining a pool of reusable server connections, reducing resource consumption and letting PostgreSQL handle far more concurrent clients than its native &lt;code&gt;max_connections&lt;/code&gt; would otherwise allow.&lt;/p&gt;

&lt;p&gt;It's best-practice to keep the count of direct connections to Postgres small. Tens of connections for smaller instances. Hundreds for larger servers.&lt;/p&gt;

&lt;p&gt;This is too restrictive for the way modern apps are built. We frequently want thousands of simultaneous connections to the database. PgBouncer gives Postgres that capability while keeping the total number of forked processes low.&lt;/p&gt;

&lt;p&gt;At PlanetScale, we recommend using PgBouncer for all application traffic, only resorting to direct connections for administrative tasks and a few other narrow cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use PgBouncer
&lt;/h2&gt;

&lt;p&gt;PgBouncer maintains a pool of pre-established Postgres server connections. When an app / client needs a database connection, it connects to PgBouncer, and then PgBouncer uses one of the pre-existing pooled connections to pass along the message. When the client is done, the connection returns to the pool for reuse. A single pooled Postgres connection can serve hundreds or thousands of client PgBouncer connections over its lifetime.&lt;/p&gt;

&lt;p&gt;When all pool connections are in use, PgBouncer queues the client until one becomes available rather than rejecting it. If the wait exceeds &lt;code&gt;query_wait_timeout&lt;/code&gt; (default: 120 seconds), the client is disconnected with an error.&lt;/p&gt;

&lt;p&gt;Whereas the Postgres default port is &lt;code&gt;5432&lt;/code&gt;, PgBouncer defaults to &lt;code&gt;6432&lt;/code&gt;. Typically, switching from a direct connection to a PgBouncer connection is as simple as switching the port in your client connection string.&lt;/p&gt;

&lt;p&gt;This is true on PlanetScale, with a twist: We give you &lt;em&gt;three options&lt;/em&gt; for using PgBouncer:&lt;/p&gt;

&lt;h3&gt;
  
  
  Local PgBouncer
&lt;/h3&gt;

&lt;p&gt;Every Postgres database includes a local PgBouncer running on the same server as the primary. Connect using the same credentials as usual, just swap the port to &lt;code&gt;6432&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Dedicated primary PgBouncer
&lt;/h3&gt;

&lt;p&gt;A dedicated primary PgBouncer runs on separate nodes from Postgres, making for better HA characteristics. It connects to the local PgBouncer first, which then connects to Postgres. Client connections persist through resizes, upgrades, and most failovers. Connect by appending &lt;code&gt;|your-pgbouncer-name&lt;/code&gt; to your username on port &lt;code&gt;6432&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Dedicated Replica PgBouncer
&lt;/h3&gt;

&lt;p&gt;Dedicated replica PgBouncers are similar to dedicated primary ones, but connect to the replicas instead (and don't route through the local bouncer).&lt;/p&gt;

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

&lt;p&gt;We recommend this if your applications make heavy use of replicas for read queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three pooling modes
&lt;/h2&gt;

&lt;p&gt;PgBouncer operates in one of three modes.&lt;/p&gt;

&lt;p&gt;Session pooling assigns a server connection for the lifetime of the client connection, releasing it only when the client disconnects. This means there's a 1:1 mapping between client and server connections. It's not incredibly useful, as it does little to reduce Postgres connection count. At times, it's helpful for limiting thundering herds of connections.&lt;/p&gt;

&lt;p&gt;Statement pooling assigns a server connection for a single SQL statement and releases it immediately after. This means multi-statement transactions are disallowed entirely. Most apps need this, so not useful in 99% of cases!&lt;/p&gt;

&lt;p&gt;Transaction pooling is the only sensible option. It assigns a server connection for the duration of a transaction, returning it to the pool the moment a COMMIT or ROLLBACK completes. This is great for most use cases, though there are a few &lt;a href="https://www.pgbouncer.org/features.html" rel="noopener noreferrer"&gt;unsupported features&lt;/a&gt; in this mode.&lt;/p&gt;

&lt;p&gt;PlanetScale only supports Transaction pooling, given the clear weaknesses of the two. When you absolutely need one of those few unsupported features, keep them to a small number of direct-to-Postgres connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knob all the things
&lt;/h2&gt;

&lt;p&gt;PgBouncer's configuration centers on a hierarchy of connection limits. These control how many client connections are accepted, how many server connections are maintained per pool, and how those relate to PostgreSQL's own &lt;code&gt;max_connections&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The connection chain works like this:&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;max_client_conn&lt;/code&gt; is the maximum number of application connections PgBouncer will accept. Because connections are lightweight in PgBouncer, this is frequently set in the 1000s.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;default_pool_size&lt;/code&gt; controls the number of server connections per (user, database) pair that PgBouncer will make to Postgres. How to configure this depends quite a bit on your schema and access patterns. In an environment where you have a single server with many logical databases and many Postgres users, this will likely need to be set low, between 1-20. When you have a single logical database and a small number of Postgres roles, this can be set much higher.&lt;/p&gt;

&lt;p&gt;The total potential PgBouncer ↔ Postgres connections equals &lt;code&gt;num_pools × default_pool_size&lt;/code&gt;. With 4 users and 2 databases we get &lt;code&gt;4 x 2 = 8 pools&lt;/code&gt;. At a pool size of 20, PgBouncer could open up to 160 connections to PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;max_db_connections&lt;/code&gt; and &lt;code&gt;max_user_connections&lt;/code&gt; are hard caps that span across all PgBouncer pools for a given database or user, respectively. They act as safety valves to prevent pool arithmetic from exceeding PostgreSQL limits. These default to 0 (no limit) but can be set in some scenarios for safety.&lt;/p&gt;

&lt;p&gt;All the above are &lt;em&gt;PgBouncer&lt;/em&gt; settings. The key setting on the Postgres side is &lt;code&gt;max_connections&lt;/code&gt;. The total server connections must stay below this number. We should always keep a few available direct connections reserved for admin tasks and other emergency scenarios. &lt;strong&gt;We NEVER want PgBouncer to use all of the connections!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All of this can be summarized in a nice formula:&lt;/p&gt;

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

&lt;p&gt;In Postgres, we can explicitly set &lt;a href="https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-SUPERUSER-RESERVED-CONNECTIONS" rel="noopener noreferrer"&gt;superuser_reserved_connections&lt;/a&gt;, which is handy for ensuring some connections are reserved for the superuser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning examples
&lt;/h2&gt;

&lt;p&gt;Thinking through some practical scenarios makes this easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small server
&lt;/h3&gt;

&lt;p&gt;First, let's think through having a PlanetScale &lt;code&gt;PS-80&lt;/code&gt; (1 vCPU, 8GB RAM per node), a single multi-tenant database, and 3 distinct Postgres users we use for clients connecting through PgBouncer: one for the app servers (&lt;code&gt;app&lt;/code&gt;), one for an analytics service (&lt;code&gt;analytics&lt;/code&gt;), and one for a data exporter (&lt;code&gt;export&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;We want to keep direct Postgres connections low, so we set the Postgres &lt;code&gt;max_connections=50&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Though it's a small database, we sometimes have 100s of app servers making simultaneous connections during peak load. We set the PgBouncer &lt;code&gt;max_client_conn=500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The majority of these connections come from a single Postgres user + database pair (the app-server user connecting to the main logical database). Because of this, we set &lt;code&gt;default_pool_size=30&lt;/code&gt; but then also set &lt;code&gt;max_user_connections=30&lt;/code&gt; and &lt;code&gt;max_db_connections=40&lt;/code&gt;. This prevents connections from the app user from utilizing all of the backend connections, ensuring some are always available for the other two. This also means PgBouncer can never hold more than 40 connections to Postgres in total, ensuring 10 are always available for other services or administrative tasks.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Large server
&lt;/h3&gt;

&lt;p&gt;Now for the same scenario, but with much higher traffic, requiring an &lt;code&gt;M-2650&lt;/code&gt; (32 vCPU, 256GB RAM per node). We'll again have the same 3 distinct Postgres users.&lt;/p&gt;

&lt;p&gt;Just because we now have 32x the CPU power, we don't want to increase direct Postgres connections by 32x. It's still wise to keep this on the lower side, so we will settle in at a max of &lt;code&gt;max_connections=500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We now sometimes have 1000s of app servers making simultaneous connections during peak load. We set the PgBouncer &lt;code&gt;max_client_conn=10000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because of this, we set &lt;code&gt;default_pool_size=200&lt;/code&gt; but then also set &lt;code&gt;max_user_connections=200&lt;/code&gt; and &lt;code&gt;max_db_connections=450&lt;/code&gt; for similar reasons as the previous example. No one user can use more than 200 connections.&lt;/p&gt;

&lt;p&gt;This also means PgBouncer can never hold more than 450 connections to Postgres, ensuring 50 remain available for other purposes, or if we add services requiring features of direct connections like session variables.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Single-tenant configuration
&lt;/h3&gt;

&lt;p&gt;Though single-tenant architectures are generally discouraged, some organizations prefer this or have inherited such a structure. In this case, we'll assume there is a unique logical database co-located on the same Postgres server for every customer.&lt;/p&gt;

&lt;p&gt;Say in this case we have a PlanetScale &lt;code&gt;M-1280&lt;/code&gt; (16 vCPUs, 128GB RAM per node), 200 distinct logical databases (for 200 tenants) and a unique Postgres role for each, for the sake of isolating permissions. There is a 1:1 mapping between each logical database and the Postgres user querying it.&lt;/p&gt;

&lt;p&gt;This is a much different connection pattern than the previous example. We have 200 roles connecting to 200 logical databases all on the same host, and want to ensure we can scale to thousands of combined connections without hitting limits.&lt;/p&gt;

&lt;p&gt;We'll center this around &lt;code&gt;max_connections=400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If any one tenant peaks at 20 connections, then we'll set PgBouncer's &lt;code&gt;max_client_conn=5000&lt;/code&gt; (includes a bit of buffer).&lt;/p&gt;

&lt;p&gt;Recall that &lt;code&gt;default_pool_size&lt;/code&gt; controls connections per (user, database) pool. Since each of the 200 users connects to exactly one database, there are 200 active pools. Even a modest &lt;code&gt;default_pool_size results&lt;/code&gt; in a large number of server connections: for example, a &lt;code&gt;default_pool_size&lt;/code&gt; of 10 would yield a theoretical max of &lt;code&gt;200 × 10 = 2,000&lt;/code&gt; server connections, far exceeding &lt;code&gt;max_connections=400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll set &lt;code&gt;default_pool_size=2&lt;/code&gt; (at most 2 PgBouncer &amp;lt;-&amp;gt; Postgres connections per pool). Since we have a clean user-to-logical-database mapping, we also set &lt;code&gt;max_db_connections=2&lt;/code&gt; and max_user_connections=2 to enforce this per-pool cap. The maximum total PgBouncer server connections is &lt;code&gt;200 × 2 = 400&lt;/code&gt;, matching &lt;code&gt;max_connections=400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A single tenant can have 10s or even 100s of connections to PgBouncer, but all these will get multiplexed through at most 2 direct Postgres connections.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  App-side PgBouncers
&lt;/h2&gt;

&lt;p&gt;In some deployments, it also makes sense to layer PgBouncer. You can run one PgBouncer on the app or client side to funnel many worker or process connections into a smaller egress set, then run another PgBouncer near Postgres as the final funnel into a tightly controlled number of direct database connections.&lt;/p&gt;

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

&lt;p&gt;This is especially useful when you need connection pooling both close to compute and close to the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple PgBouncers
&lt;/h2&gt;

&lt;p&gt;In large-scale deployments, setting up multiple PgBouncers is useful for traffic isolation. When your web app, background workers, and other consumers all share one pool, a spike from one class of traffic can saturate the PgBouncer and delay everything else.&lt;/p&gt;

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

&lt;p&gt;Giving each major consumer its own PgBouncer creates independent funnels with their own limits, pool sizing, and failure domains. That makes it easier to protect latency-sensitive app traffic from bursty worker traffic and tune each workload separately.&lt;/p&gt;

&lt;p&gt;For an additional layer of protection, &lt;a href="https://planetscale.com/blog/introducing-database-traffic-control" rel="noopener noreferrer"&gt;Database Traffic Control™&lt;/a&gt; lets you enforce resource budgets on query traffic by pattern, application name, Postgres user, or custom tags — without needing separate infrastructure. The two approaches complement each other well: PgBouncer manages connections, Traffic Control manages resource consumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key concepts
&lt;/h2&gt;

&lt;p&gt;PgBouncer solves a fundamental architectural constraint in PostgreSQL: the process-per-connection model that makes every connection expensive. When working with PgBouncer, there are a few fundamental things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transaction pooling is the mode that matters. Every transaction, be it a single query or many, gets a dedicated connection from PgBouncer &amp;lt;-&amp;gt; Postgres while executing. After this, the connection can be re-used for another transaction, maybe on the same client, and maybe for another.&lt;/li&gt;
&lt;li&gt;Use PgBouncer as much as possible. If you absolutely need features that are incompatible with transaction pooling, like &lt;code&gt;LISTEN&lt;/code&gt;, session-level &lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;RESET&lt;/code&gt;, or &lt;code&gt;SQL PREPARE&lt;/code&gt;/&lt;code&gt;DEALLOCATE&lt;/code&gt;, use a direct connection. In all other cases, the small latency penalty of PgBouncer is well worth the scalability and connection safety.&lt;/li&gt;
&lt;li&gt;The key configs to pay attention to are: &lt;code&gt;max_connections&lt;/code&gt; (Postgres), plus &lt;code&gt;max_client_conn&lt;/code&gt;, &lt;code&gt;default_pool_size&lt;/code&gt;, &lt;code&gt;max_db_connections&lt;/code&gt;, and &lt;code&gt;max_user_connections&lt;/code&gt; (PgBouncer).&lt;/li&gt;
&lt;li&gt;Ensure things are configured to allow for direct connections, even when all PgBouncer connections are in use.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>planetscale</category>
      <category>database</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Behind the Scenes: How Database Traffic Control Works</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Wed, 01 Apr 2026 19:13:49 +0000</pubDate>
      <link>https://dev.to/planetscale/behind-the-scenes-how-database-traffic-control-works-20pe</link>
      <guid>https://dev.to/planetscale/behind-the-scenes-how-database-traffic-control-works-20pe</guid>
      <description>&lt;p&gt;&lt;em&gt;By Patrick Reynolds&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In March, we released Database Traffic Control™, a feature for mitigating and preventing database overload due to unexpectedly expensive SQL queries. For an overview, &lt;a href="https://planetscale.com/blog/introducing-database-traffic-control" rel="noopener noreferrer"&gt;read the blog post introducing the feature&lt;/a&gt;, and to get started using it, read the &lt;a href="https://planetscale.com/docs/postgres/traffic-control/" rel="noopener noreferrer"&gt;reference documentation&lt;/a&gt;. This post is a deep dive into how the feature works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;If you already know how Postgres and Postgres extensions work internally, you can skip this section.&lt;/p&gt;

&lt;p&gt;A single Postgres server is made up of many running processes. Each client connection to Postgres gets its own dedicated worker &lt;a href="https://planetscale.com/blog/processes-and-threads" rel="noopener noreferrer"&gt;process&lt;/a&gt;, and all SQL queries from that client connection run, one at a time, in that worker process. When a client sends a SQL query, the worker process parses it, plans it, executes it, and sends any results back to the client. &lt;a href="https://planetscale.com/blog/what-is-a-query-planner" rel="noopener noreferrer"&gt;Planning&lt;/a&gt; is a key step, in which Postgres takes a parsed query and turns it into a step-by-step execution plan that specifies the indexes to use, the order to load rows from multiple tables, and the operators that will be used to filter, aggregate, and join those rows. Most queries can be run using several different plans, so it's the planner's job to estimate the cost of the possible plans and pick the cheapest one.&lt;/p&gt;

&lt;p&gt;Every part of how Postgres handles queries can be modified by extensions. Extensions can add new functions, new data types, new storage systems, and new authentication methods, among other things. (They can also &lt;a href="https://www.vldb.org/pvldb/vol18/p1962-kim.pdf" rel="noopener noreferrer"&gt;add new failure modes&lt;/a&gt;, but that's a topic for another day.) Extensions can also passively observe and report on traffic, like PlanetScale's own &lt;code&gt;pginsights&lt;/code&gt; extension that powers &lt;a href="https://planetscale.com/docs/postgres/monitoring/query-insights" rel="noopener noreferrer"&gt;Query Insights&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Much of what Postgres extensions can do, they do using hooks. A hook is a function that runs before, after, or instead of existing Postgres functionality. Want to observe or replace the planner? There's a hook for that. Want to examine queries as they execute? There are three hooks for that. As of this writing, there are &lt;a href="https://github.com/search?q=repo%3Apostgres%2Fpostgres%20%2F%5E%5CS.*%5Cw_hook%20%3D%20NULL%2F&amp;amp;type=code" rel="noopener noreferrer"&gt;55 hooks&lt;/a&gt; available to anyone writing Postgres extensions.&lt;/p&gt;

&lt;p&gt;PlanetScale's &lt;code&gt;pginsights&lt;/code&gt; extension installs hooks for the &lt;code&gt;ExecutorRun&lt;/code&gt; and &lt;code&gt;ProcessUtility&lt;/code&gt; functions, among others, to run timers and measure resource consumption while SQL statements execute. Since each hook wraps the original Postgres functionality, that means &lt;code&gt;pginsights&lt;/code&gt; sees each query just before it executes and again just after it completes. Any time that has elapsed and any resources the worker process has consumed are directly attributable to that query. The extension does some aggregation, sends aggregate data periodically to a data pipeline, and returns control to Postgres to accept the next query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insights, hooks, and blocking queries
&lt;/h2&gt;

&lt;p&gt;When we first started planning for Traffic Control, we knew we would use a Postgres extension with a hook on &lt;code&gt;ExecutorRun&lt;/code&gt; to decide whether or not each statement would be allowed to run. Initially, we wrote a new extension for this. We soon realized that there are two ways to choose which queries to block: based on static analysis of the individual query, or based on cumulative measurements of resource usage over time. We split the extension along those lines. Blocking based on static analysis got merged into the project that became &lt;code&gt;pg_strict&lt;/code&gt;. Blocking based on cumulative resource usage became Traffic Control.&lt;/p&gt;

&lt;p&gt;It turns out Traffic Control needed the same hook points and much of the same information that &lt;code&gt;pginsights&lt;/code&gt; already had. So rather than duplicate all that code and impose the extra runtime overhead of another extension, we taught &lt;code&gt;pginsights&lt;/code&gt; how to block queries.&lt;/p&gt;

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

&lt;p&gt;If there are any Traffic Control rules configured, then at the beginning of each query execution, the extension does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It identifies all of the rules that match the &lt;a href="https://planetscale.com/docs/postgres/traffic-control/concepts#rules" rel="noopener noreferrer"&gt;tags and other metadata&lt;/a&gt; of the query. Each rule identifies a budget; multiple rules can map to the same budget.&lt;/li&gt;
&lt;li&gt;It checks to see if any of the applicable budgets has reached its concurrency limit.&lt;/li&gt;
&lt;li&gt;It checks if the query's estimated cost is higher than any applicable budget's per-query limit.&lt;/li&gt;
&lt;li&gt;It checks to see if every applicable budget has enough available capacity for the query to begin execution. In the &lt;a href="https://planetscale.com/docs/postgres/traffic-control/concepts#resource-budget-limits" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, these parameters are described as the burst limit and the server share. As we'll see &lt;a href="https://planetscale.com/blog/behind-the-scenes-how-traffic-control-works#leaky-buckets" rel="noopener noreferrer"&gt;below&lt;/a&gt;, those parameters combine over time to describe the behavior of a leaky-bucket rate limiter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any budget fails any of these checks, then the query is warned or blocked, based on how the budget is configured.&lt;/p&gt;

&lt;p&gt;Blocking a query just before it begins execution means the server spends no resources on the query, beyond the cost of the planner and the decision to block it. That's an improvement over schedulers like &lt;a href="https://www.man7.org/linux/man-pages/man7/cgroups.7.html" rel="noopener noreferrer"&gt;Linux cgroups&lt;/a&gt;, which let every task begin and simply starve them of resources if higher priority tasks exist in the system. It's also an improvement over the &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; &lt;code&gt;statement_timeout&lt;/code&gt; setting, which allows any overly expensive query to consume resources until it times out. Traffic Control blocks expensive, low priority queries before they begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost prediction
&lt;/h2&gt;

&lt;p&gt;I glossed over something important in the last section: cost. The concurrency check is easy, because it just counts worker processes already assigned to the queries associated with a Traffic Control budget. But the other two checks — per-query cost and cumulative cost — require us to know what resources the query will consume before it even begins execution. How do we do that? We trust, but also don't trust, the planner.&lt;/p&gt;

&lt;p&gt;A SQL query planner takes a parsed SQL statement and selects what it hopes is the most efficient series of steps to execute that query. To evaluate all the possible plans, the planner has to estimate the cost of each one. When you run &lt;code&gt;EXPLAIN&lt;/code&gt; on a SQL statement, Postgres's planner shows the cost of each step in the chosen plan, as well as the overall total cost. The cost is &lt;a href="https://www.postgresql.org/docs/current/runtime-config-query.html#RUNTIME-CONFIG-QUERY-CONSTANTS" rel="noopener noreferrer"&gt;measured in dimensionless units and is based on configurable weights&lt;/a&gt; assigned to each step the plan will take. There are a lot of variables that go into the plan cost, most of which you can ignore for the purposes of understanding Traffic Control. Just remember these two things: plan costs are roughly linear (a plan with double the cost should take something like double the time and resources to execute), and the relationship between plan costs and real-world resources is heavily dependent on what query you're running, what server you run it on, and what else is happening on that server at the moment.&lt;/p&gt;

&lt;p&gt;Traffic Control compensates for those dependencies. We assume that there is an unknown constant k that we can multiply the plan cost by, to get the actual wall-clock time it will take to execute that query. But that constant is different for each &lt;a href="https://planetscale.com/blog/query-performance-analysis-with-insights" rel="noopener noreferrer"&gt;query pattern&lt;/a&gt; and for each host. The constant may also change over time as the workload mix on the server changes and as tables grow and change. So it's not exactly a constant!&lt;/p&gt;

&lt;p&gt;Traffic Control implements a hash table on each host, mapping query patterns to two averages: CPU time and planner cost estimates. Both are exponential moving averages, heavily weighting recent queries. Every time a query completes, we update both of those averages. The magical not-quite-constant k is the ratio of the two.&lt;/p&gt;

&lt;p&gt;Each time a query comes in, Traffic Control multiplies the planner's estimated cost by k to guess how much CPU and/or wall-clock time the query will take. Based on that estimate, Traffic Control decides if the query can be allowed to begin. If it does, then at the end of query execution, Traffic Control updates the two averages for that query pattern so the k value will be more recent and more precise for the next query that arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaky buckets
&lt;/h2&gt;

&lt;p&gt;Two of the checks that Traffic Control performs for each query are easy: if the query's estimated cost is too high, block it. If too many queries in the same budget are already running, block it. But the final check — is there enough capacity in the budget to proceed — is harder. It's important, though! Many executions of a moderately expensive query can be even more damaging than a single very expensive query, and managing a budget over time is the best way to block queries that are only expensive in aggregate. Traffic Control considers the cumulative cost of queries in each configured budget.&lt;/p&gt;

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

&lt;p&gt;Each budget is modeled as a reverse leaky bucket. Here's how that works. Each query that executes accumulates debt in the bucket. Any query that would cause the bucket to overflow with debt is blocked. Debt drains out over time, until the bucket is empty. The bucket has &lt;a href="https://planetscale.com/docs/postgres/traffic-control/concepts#resource-budget-limits" rel="noopener noreferrer"&gt;two important parameters&lt;/a&gt;: its size and its drain rate. The size dictates the burst limit, or what total resources queries under a given budget can use in a short amount of time. The drain rate dictates the server share, or what fraction of overall resources queries under a given budget can use in the long term.&lt;/p&gt;

&lt;p&gt;Traditionally, leaky buckets work the other way: they start out full, they fill (but never overflow) with credits at a configured rate, traffic consumes credits, and if a bucket is ever empty, traffic gets blocked. We inverted the model for a simple reason: an empty bucket doesn't need to be stored. Over time, we may need to store many buckets for changing rules and changing query metadata. We can drop buckets with a zero debt level, meaning that we only need to store recently active buckets, instead of every possible bucket. We store as many buckets as will fit in a configurable amount of shared memory, and we evict them implicitly when their debt falls to zero.&lt;/p&gt;

&lt;p&gt;There is no periodic task that drains debt from all buckets. Instead, each bucket is updated only when read. There is also no periodic task to evict buckets with a debt level of zero. Instead, adding a new bucket to the table evicts any that have already emptied, or whichever bucket is expected to become empty soonest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule sets
&lt;/h2&gt;

&lt;p&gt;One important goal for Traffic Control is that it can efficiently decide when not to block a query. After all, Traffic Control has to make that decision before each query is even allowed to begin execution. So the budget here is measured in microseconds. But we also want developers and database administrators to be able to configure as many rules as it takes to manage traffic to their application. So it's crucial that we can evaluate many rules quickly. Enter rule sets: a data structure that allows evaluating &lt;code&gt;n&lt;/code&gt; rules in &lt;code&gt;O(1)&lt;/code&gt; time.&lt;/p&gt;

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

&lt;p&gt;Each rule has the form &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt;, and it matches any query that has that same value for that same key. It's complicated a bit by the fact that value can be an IP address with a CIDR mask.&lt;/p&gt;

&lt;p&gt;A rule set maps each &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt; pair to a rule. Now, when a query comes in with metadata like &lt;code&gt;username=postgres, app=commerce, controller=api&lt;/code&gt;, the rule set can quickly identify the rule for each of those pairs. Hence, for this query, there are just three lookups in the rule set, regardless of how many rules are configured.&lt;/p&gt;

&lt;p&gt;Note that a rule set only &lt;em&gt;identifies rules to consider&lt;/em&gt;. Each rule's budget is only checked if all its conditions match the query. A rule set is all about checking as few rules as possible. So, the sequence is: the rule set identifies a list of rules, that list is narrowed down to just the rules that actually match, and then the budgets for all the matching rules get checked to see if the query can proceed.&lt;/p&gt;

&lt;p&gt;There are three exceptions to the &lt;code&gt;O(1)&lt;/code&gt; target for identifying rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rules for the &lt;code&gt;remote_address&lt;/code&gt; key check for a match for each mask length. So if you have rules for ten different mask lengths, the rule set has to do as many as ten lookups to find the rule with the longest matching prefix.&lt;/li&gt;
&lt;li&gt;Any conjunction rule — that is, a rule with multiple &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt; pairs ANDed together — may be identified as a candidate for queries that match any one of the &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt; pairs in the rule. So if you have conjunction rules with overlapping &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt; pairs, the rule set may identify several or all of them as candidates for each query.&lt;/li&gt;
&lt;li&gt;It is possible to add multiple rules for the exact same &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt; pair. If you do that, any query with that exact &lt;code&gt;&amp;lt;key, value&amp;gt;&lt;/code&gt; pair will get checked against all of those rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Applying new rules
&lt;/h2&gt;

&lt;p&gt;Traffic Control is meant to be used both proactively and during incident response. For incident response, it's important that rules take effect quickly. And they do! Rules created or modified in the UI generally take effect at all database replicas in just 1-2 seconds. How?&lt;/p&gt;

&lt;p&gt;Rules and budgets are stored as objects in the PlanetScale app. Any change to Traffic Control rules made in the UI or the API gets stored as rows in the &lt;code&gt;planetscale&lt;/code&gt; database. Then it's serialized as JSON in the &lt;code&gt;traffic_control.rules&lt;/code&gt; and &lt;code&gt;traffic_control.budgets&lt;/code&gt; parameters for Postgres. Some Postgres parameters require restarting the server, but those two don't. So they cut the line and get sent immediately to postgresql.conf files on each database replica. Postgres reads the new config, and each worker process parses it into a rule set as soon as it completes whatever query it's executing. The rule set is in place before the next query begins.&lt;/p&gt;

&lt;p&gt;One big advantage of using Postgres configuration files, rather than sending configuration over SQL connections, is robustness on a busy server. You may want new Traffic Control rules most urgently when Postgres is using 100% of its available CPU, 100% of its worker processes, or both. Changing config files is possible even when opening a new SQL connection and issuing statements wouldn't be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Traffic Control uses the hooks and the performance measurements that Query Insights already implemented, then bolts on a system for sorting query traffic into budgets and warning or blocking queries that exceed those budgets. Each query can be warned or blocked if it's individually too expensive, if too many other queries are already running under the same budget, or if recent and concurrent queries under the same budget have consumed too many resources in the aggregate. Traffic Control implements a dynamic model per query pattern that leverages the existing Postgres planner to estimate the real-world cost of a query before it begins to execute. Leaky buckets impose limits on both traffic bursts and the long-term average fraction of server resources assigned to any individual budget.&lt;/p&gt;

&lt;p&gt;Taken as a whole, these elements implement Traffic Control, which gives developers and database administrators powerful new tools to identify, prioritize, and limit SQL traffic.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>planetscale</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>AI Fatigue Has Entered the Chat: How to Innovate Without Alienating Your Brand</title>
      <dc:creator>Meg528</dc:creator>
      <pubDate>Sun, 03 Nov 2024 17:24:04 +0000</pubDate>
      <link>https://dev.to/meg528/ai-fatigue-has-entered-the-chathow-to-innovate-without-alienating-your-brand-5pm</link>
      <guid>https://dev.to/meg528/ai-fatigue-has-entered-the-chathow-to-innovate-without-alienating-your-brand-5pm</guid>
      <description>&lt;p&gt;It wasn’t &lt;em&gt;that&lt;/em&gt; long ago that AI was something sensationalized mostly by high-budget movies like &lt;em&gt;The Matrix&lt;/em&gt;. In 2024, however, we’re not living in a mind-bending alternate reality, dodging bullets and Agent Smith. Instead, we’re using artificial intelligence to optimize our blogs for search engines, create lifelike videos without any human actors, and write code within seconds to power the next app to hit the marketplace.&lt;/p&gt;

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

&lt;p&gt;In my own day-to-day work, I’m exploring how to use AI to get more done, better — a less-than-frictionless transition since my background is in writing. (When AI blew up, writers feared that they’d be the first on the chopping block, and in some cases, they were.)&lt;/p&gt;

&lt;p&gt;Brands raced to jump on the AI bandwagon — some, a little recklessly. A couple of years later, many are starting to feel the blowback: customers who want to talk to a human being, not an AI chatbot; people who want to read human-written words, not AI-generated; search engines penalizing websites for page after page of low-quality content; users who are struggling with the inescapably prolific amount of AI-created content in both Google and social media news feeds.&lt;/p&gt;

&lt;p&gt;AI fatigue is here. What is it, what are the implications, and what can we do about it?&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AI Fatigue?
&lt;/h2&gt;

&lt;p&gt;The term “AI fatigue” refers to a general hesitation toward, lack of excitement for, or even suspicion or skepticism around using AI-driven technologies.&lt;/p&gt;

&lt;p&gt;I experienced this myself very recently, while using a healthcare provider’s AI chatbot. I had the option to answer a series of questions and potentially get the information I needed, or I could speak with a representative directly. I wasn’t in the most patient mood and immediately opted for the human route. Why? Simple. The last few times I engaged with AI chatbots were a complete bust. (To be clear, I believe that AI chatbots can work stupendously and have happily utilized them in other moments.)&lt;/p&gt;

&lt;p&gt;So, while organizations are racing to adopt AI solutions, customers might be whistling a different tune.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Caused AI Fatigue?
&lt;/h2&gt;

&lt;p&gt;The speed and intensity with which technology is progressing make it hard for some of us to keep up.&lt;/p&gt;

&lt;p&gt;Think of older generations trying to figure out Facebook. Now, add AI on top of that. And technology is only gaining momentum. Some estimates [1] say that computers’ speed and power have typically doubled every 1.5 to two years since the 1960s.&lt;/p&gt;

&lt;p&gt;The proof is in the pudding: About 90% of the world’s data [2] was generated within the last few years alone.&lt;/p&gt;

&lt;p&gt;AI technology shows no signs of slowing down, which means we have to hustle to keep up. And, put simply, many people are tired of trying to do that. We’re always working so hard to try to understand the next big thing that we barely have an opportunity to slow down and just… be.&lt;/p&gt;

&lt;p&gt;Interestingly, the Gartner Hype Cycle [3] for artificial intelligence reports that the hype around AI has far outweighed what the technology has actually delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Gone Astray: When Technology Backfires
&lt;/h2&gt;

&lt;p&gt;It feels like the widespread response from countless companies has been, “More AI!” And to be fair, in many cases, artificial intelligence has completely revolutionized the way some businesses are run and the experience they provide for their users.&lt;/p&gt;

&lt;p&gt;But not always.&lt;/p&gt;

&lt;p&gt;You might remember when CNET [4] was found to be publishing AI-generated articles in a less-than-transparent manner. The byline of these articles read “CNET Money Staff.” If you clicked on that byline, a popup appeared disclosing that the content was written by AI. To make matters worse, we then learned that more than half of these AI-generated articles contained significant errors and plagiarism.&lt;/p&gt;

&lt;p&gt;When CNET’s parent company, Red Ventures, went to sell it, the blemish on their reputation was a hurdle — although they did eventually sell it [5] for over $100 million. (Some sources say it was closer to $250 million [6].) This was after paying $500 million for it four years earlier.&lt;/p&gt;

&lt;p&gt;This is just one example of what can happen when we get greedy with AI. The ripple effect can be ghastly for both your bottom line and the people working to keep the lights on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Do We Go From Here?
&lt;/h2&gt;

&lt;p&gt;So, you now know what AI fatigue is. You’ve read some of the horror stories. Should you abandon AI completely? Absolutely not. For every AI failure, we can talk about many successes.&lt;/p&gt;

&lt;p&gt;Plus, this technology isn’t going anywhere. We have two choices: Embrace it, or get left behind.&lt;/p&gt;

&lt;p&gt;But here’s the key: Using it &lt;em&gt;intentionally&lt;/em&gt; is critical.&lt;/p&gt;

&lt;p&gt;What does this look like? Let’s go through some tips and examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Recognize That There’s a Time and a Place
&lt;/h3&gt;

&lt;p&gt;The solution to all our woes is not to replace everything with AI. The approach should be much more purposeful.&lt;/p&gt;

&lt;p&gt;I spoke with Apoorva Joshi [7], Senior AI Developer Advocate at MongoDB, who said, “The future isn’t about AI replacing humans; it’s about humans and AI working together. The path to success lies in collaboration, where human creativity and intelligence are enhanced by AI’s ability to drive innovation and help solve complex problems.”&lt;/p&gt;

&lt;p&gt;As one example, when it comes to content production, AI can be an excellent complement, rather than a replacement. MongoDB’s Developer Center [8] is a valuable resource for developers around the globe. While these authors may use AI to formulate ideas, the content is written, reviewed, and fact-checked by humans. Why? Well, put simply, at the end of the day, these authors are responsible for the content. If something goes awry, “AI did it!” is no excuse.&lt;/p&gt;

&lt;p&gt;Plus, humans do it better.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Prioritize Quality Over Quantity
&lt;/h3&gt;

&lt;p&gt;AI coding assistants have completely changed the way we build applications, speeding up the development time and taking away a lot of the heavy lifting. The same can be said for the use of AI in content production.&lt;/p&gt;

&lt;p&gt;Because the barrier to entry is now much lower, what we’ve seen is a &lt;em&gt;huge&lt;/em&gt; surge in the number of apps hitting the market, blogs on search engine results pages, and videos going live on YouTube. This would be a positive change if more of these apps, blogs, and videos were of a better quality. Instead, many of us find ourselves struggling to swim through a flood of junk.&lt;/p&gt;

&lt;p&gt;Take note: Producing more of something that’s low-quality doesn’t make it higher-quality. If you stop caring about creating amazing things and simply focus on creating more things, users will notice, and they will go somewhere else to find something better.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Keep the End Result in Mind
&lt;/h3&gt;

&lt;p&gt;If you use AI in any capacity to build something, it doesn’t change what your ultimate goal should be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Entertain your users.&lt;/li&gt;
&lt;li&gt;Educate your users.&lt;/li&gt;
&lt;li&gt;Solve a problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can’t answer how your product/service does one or more of these things, your job isn’t done.&lt;/p&gt;

&lt;p&gt;AI can be used to create personalized songs that you can then gift to people you care about. We call that entertainment!&lt;/p&gt;

&lt;p&gt;Reliable AI chatbots empower users by delivering relevant help docs so that they don’t have to wait in long queues — an excellent way to educate users and help them help themselves.&lt;/p&gt;

&lt;p&gt;Vector search [9] allows users to find search results based not just on how well their keywords match but on the &lt;em&gt;meaning&lt;/em&gt; behind them. Better search results, faster? Problem solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  An AI Reset: Moving Forward With Renewed Energy
&lt;/h2&gt;

&lt;p&gt;AI fatigue doesn’t have to be permanent, but we do need to shift our approach.&lt;/p&gt;

&lt;p&gt;By this point, we’ve got at least a basic understanding of just how powerful AI is and what it’s capable of. We’ve tested it and applied it in countless ways. Some have been miraculous, and others have been disastrous.&lt;/p&gt;

&lt;p&gt;Next, we iterate!&lt;/p&gt;

&lt;p&gt;Using AI at the right time, under the right circumstances, and always for the betterment of users — consider this your north star, and you’ll never go wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.zippia.com/answers/is-technology-growing-exponentially/" rel="noopener noreferrer"&gt;https://www.zippia.com/answers/is-technology-growing-exponentially/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://leftronic.com/blog/how-fast-is-technology-growing-statistics" rel="noopener noreferrer"&gt;https://leftronic.com/blog/how-fast-is-technology-growing-statistics&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.gartner.com/en/documents/5505695" rel="noopener noreferrer"&gt;https://www.gartner.com/en/documents/5505695&lt;/a&gt;&lt;br&gt;
&lt;a href="https://futurism.com/cnet-for-sale-ai" rel="noopener noreferrer"&gt;https://futurism.com/cnet-for-sale-ai&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.charlotteobserver.com/news/business/article290791529.html" rel="noopener noreferrer"&gt;https://www.charlotteobserver.com/news/business/article290791529.html&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.axios.com/2024/08/06/cnet-ziff-davis-red-ventures#" rel="noopener noreferrer"&gt;https://www.axios.com/2024/08/06/cnet-ziff-davis-red-ventures#&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/apoorvajoshi95/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/apoorvajoshi95/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://mdb.link/towards-ai-dc" rel="noopener noreferrer"&gt;https://mdb.link/towards-ai-dc&lt;/a&gt;&lt;br&gt;
&lt;a href="https://mdb.link/vector-search-towards-ai" rel="noopener noreferrer"&gt;https://mdb.link/vector-search-towards-ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>womenintech</category>
      <category>contentwriting</category>
    </item>
  </channel>
</rss>
