<?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: Adolfo Pedernera</title>
    <description>The latest articles on DEV Community by Adolfo Pedernera (@apeder).</description>
    <link>https://dev.to/apeder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4056349%2Fb7b61090-1c95-438b-81b5-e0fb8fbaab4b.jpg</url>
      <title>DEV Community: Adolfo Pedernera</title>
      <link>https://dev.to/apeder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apeder"/>
    <language>en</language>
    <item>
      <title>Database Performance for Developers: Indexing, Query Plans, and the Queries That Don't Scale</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Fri, 28 Aug 2026 01:17:03 +0000</pubDate>
      <link>https://dev.to/apeder/database-performance-for-developers-indexing-query-plans-and-the-queries-that-dont-scale-472e</link>
      <guid>https://dev.to/apeder/database-performance-for-developers-indexing-query-plans-and-the-queries-that-dont-scale-472e</guid>
      <description>&lt;p&gt;&lt;em&gt;How database indexes actually work: B-trees, selectivity, composite indexes, covering indexes, and reading EXPLAIN plans so your queries scale past 10 million rows.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Query That Worked Fine Until It Didn't
&lt;/h2&gt;

&lt;p&gt;Every developer has a story about the query that worked perfectly in development and crawled to a halt in production. The table had 10,000 rows in your local database; in production it has 10 million. The query that took 5 milliseconds now takes 45 seconds. The dashboard times out. The support tickets arrive.&lt;/p&gt;

&lt;p&gt;The culprit is almost always the same: a &lt;strong&gt;full table scan&lt;/strong&gt;. The database engine reads every single row in the table, one by one, looking for the ones that match your &lt;code&gt;WHERE&lt;/code&gt; clause. On a table with 10 million rows, that means reading millions of disk pages — work that could have been avoided with the right index.&lt;/p&gt;

&lt;p&gt;An index is a separate data structure that the database maintains alongside your table. It works like the index at the back of a textbook: instead of reading every page to find every mention of "normalization," you look up the term in the index, which points you to pages 42, 87, and 156. Database indexes do the same thing, but with a twist that catches many developers off guard: &lt;strong&gt;an index is only useful if the query planner decides to use it.&lt;/strong&gt; And the planner's decision depends on a single number: &lt;strong&gt;selectivity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This guide explains how indexes actually work, when they help, when they hurt, and how to read an &lt;code&gt;EXPLAIN&lt;/code&gt; plan to understand what your database is doing — so the next query that doesn't scale is one you catch before it reaches production.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a B-Tree Index Works
&lt;/h2&gt;

&lt;p&gt;Most database indexes are implemented as &lt;strong&gt;B-trees&lt;/strong&gt; (or their variant, the B+-tree). A B-tree is a self-balancing tree structure that keeps data sorted and allows searches, insertions, and deletions in logarithmic time.&lt;/p&gt;

&lt;p&gt;Imagine a table with 1 million rows and an index on the &lt;code&gt;email&lt;/code&gt; column. Without an index, finding a specific email requires scanning all 1 million rows. With a B-tree index, the database starts at the root node, compares the target value to the keys in the node, and follows the appropriate branch — roughly 20 comparisons for 1 million rows (log₂ of 1,000,000 ≈ 20), each reading one disk page. The difference between reading 1 million pages and 20 pages is the difference between a query that takes seconds and one that takes milliseconds.&lt;/p&gt;

&lt;p&gt;The B-tree structure also explains why indexes are sorted: range queries (&lt;code&gt;WHERE age BETWEEN 25 AND 35&lt;/code&gt;) can find the starting point in the tree and then scan sequentially through the leaf nodes. A hash index, by contrast, supports only equality lookups — fast for &lt;code&gt;WHERE email = 'x'&lt;/code&gt; but useless for ranges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost of indexing is not free.&lt;/strong&gt; Every index you add slows down &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; operations because the database must update both the table and every affected index. A table with 5 indexes takes roughly 5× longer to insert into than a table with none. The art of indexing is balancing read speed against write speed — and knowing when an index helps so little that it isn't worth the cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Selectivity: The Number That Decides Everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Selectivity&lt;/strong&gt; is the fraction of rows a filter keeps. A filter &lt;code&gt;WHERE email = 'a@b.com'&lt;/code&gt; on a unique column has selectivity ≈ 1/N (one row out of N). A filter &lt;code&gt;WHERE status = 'active'&lt;/code&gt; on a column with two values has selectivity ≈ 0.5 (half the rows). The query planner uses this number to decide: &lt;em&gt;is it cheaper to read the index and then fetch the matching rows, or just scan the whole table?&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The formula for selectivity is straightforward:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;S&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where R is the number of rows returned and N is the total rows in the table. A selectivity of 0.00001 (one row in 100,000) means the index is almost certainly used. A selectivity of 0.5 (half the rows) means the planner will likely ignore the index and scan the table directly — reading 500,000 rows via index lookups is more expensive than reading them sequentially.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/sql-index-selectivity-calculator" rel="noopener noreferrer"&gt;SQL Index Selectivity Calculator&lt;/a&gt; computes this from your table's row count, the number of distinct values in the indexed column, and the rows your query returns. It also reports the planner's likely choice (index scan vs sequential scan) and the estimated cost of each — useful for deciding whether an index on a given column will actually be used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cliff is steep.&lt;/strong&gt; Going from 100 distinct values (1% selectivity, clearly indexed) to 10 distinct values (10% selectivity, borderline) multiplies selectivity by 10×. Below 100 distinct values on a large table, a single-column index is questionable; above 1,000 it is almost always worthwhile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Composite Indexes: Order Matters
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;composite index&lt;/strong&gt; (also called a multi-column index) indexes two or more columns together. This is where most developers make their first indexing mistake: they assume the column order doesn't matter. It does.&lt;/p&gt;

&lt;p&gt;A composite index on &lt;code&gt;(last_name, first_name)&lt;/code&gt; can answer queries that filter on &lt;code&gt;last_name&lt;/code&gt; alone, or on both &lt;code&gt;last_name&lt;/code&gt; AND &lt;code&gt;first_name&lt;/code&gt;. But it &lt;strong&gt;cannot&lt;/strong&gt; answer a query that filters on &lt;code&gt;first_name&lt;/code&gt; alone. The reason is the B-tree structure: the tree is sorted by &lt;code&gt;last_name&lt;/code&gt; first, then by &lt;code&gt;first_name&lt;/code&gt; within each &lt;code&gt;last_name&lt;/code&gt;. Searching for &lt;code&gt;first_name = 'John'&lt;/code&gt; without knowing the &lt;code&gt;last_name&lt;/code&gt; is like searching for everyone named "John" in a phone book sorted by last name — you have to read the whole book.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; put the most selective column first (the one that eliminates the most rows), and put columns used in equality filters before columns used in range filters. A query &lt;code&gt;WHERE status = 'active' AND created_at &amp;gt; '2024-01-01'&lt;/code&gt; benefits from an index on &lt;code&gt;(status, created_at)&lt;/code&gt; — equality first, range second. Reversing the order to &lt;code&gt;(created_at, status)&lt;/code&gt; is less efficient because the planner can use the first column for range scanning but must filter the second column from the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; A users table with 5 million rows, 50,000 distinct cities, and 2 statuses (active/inactive). A query &lt;code&gt;WHERE city = 'Berlin' AND status = 'active'&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index on &lt;code&gt;(city, status)&lt;/code&gt;: selectivity ≈ 1/50,000 × 0.5 = 0.00001 → index used, ~100 rows fetched&lt;/li&gt;
&lt;li&gt;Index on &lt;code&gt;(status, city)&lt;/code&gt;: selectivity ≈ 0.5 × 1/50,000 = 0.00001 → same final result, but the planner may prefer the more selective column first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference becomes dramatic when the first column is not selective. An index on &lt;code&gt;(status, city)&lt;/code&gt; forces the planner to scan all 2.5 million active users first, then filter by city. An index on &lt;code&gt;(city, status)&lt;/code&gt; jumps directly to Berlin's ~100 users, then filters by status.&lt;/p&gt;




&lt;h2&gt;
  
  
  Covering Indexes: When the Index Is the Answer
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;covering index&lt;/strong&gt; is an index that contains all the columns a query needs, so the database can answer the query entirely from the index without ever touching the table. This is the fastest possible access path.&lt;/p&gt;

&lt;p&gt;Consider a query &lt;code&gt;SELECT user_id, email FROM users WHERE email = 'a@b.com'&lt;/code&gt;. With an index on &lt;code&gt;(email)&lt;/code&gt;, the database finds the row in the index, then fetches the corresponding table row to get &lt;code&gt;user_id&lt;/code&gt;. With a covering index on &lt;code&gt;(email, user_id)&lt;/code&gt;, the database finds everything it needs in the index — no table access at all. On a large table, this can be 10-100× faster because the index is smaller and more cacheable than the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; covering indexes are wider (more columns = more bytes per entry), which means fewer entries per disk page and more storage. Use them for your most frequent queries, not for every query. A good candidate is a query that runs thousands of times per minute and returns few columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL's &lt;code&gt;INCLUDE&lt;/code&gt; clause&lt;/strong&gt; (available since PostgreSQL 11) lets you add non-key columns to an index without making them part of the sort key: &lt;code&gt;CREATE INDEX ON users (email) INCLUDE (user_id)&lt;/code&gt;. This gives you the covering benefit without the sorting overhead on the included columns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading EXPLAIN: What the Planner Is Actually Doing
&lt;/h2&gt;

&lt;p&gt;Every database provides an &lt;code&gt;EXPLAIN&lt;/code&gt; command that shows the query plan — the step-by-step strategy the planner has chosen. Learning to read these plans is the single most useful skill for database performance.&lt;/p&gt;

&lt;p&gt;A typical PostgreSQL &lt;code&gt;EXPLAIN&lt;/code&gt; output for a query on a 10-million-row table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan on users (cost=0.00..183340.00 rows=1 width=36)
  Filter: (email = 'a@b.com'::text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you: the planner chose a &lt;strong&gt;sequential scan&lt;/strong&gt; (reading every row), estimated cost is 183,340 (arbitrary units), expects to return 1 row, and applies the &lt;code&gt;email&lt;/code&gt; filter to each row. The cost number is what the planner uses to compare strategies — lower is better.&lt;/p&gt;

&lt;p&gt;After adding an index on &lt;code&gt;email&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using users_email_idx on users (cost=0.43..8.45 rows=1 width=36)
  Index Cond: (email = 'a@b.com'::text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the planner uses the index, and the estimated cost dropped from 183,340 to 8.45 — a 20,000× improvement. The &lt;code&gt;Index Cond&lt;/code&gt; shows what the index is filtering on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key terms in EXPLAIN output:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Seq Scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full table scan&lt;/td&gt;
&lt;td&gt;Add an index, or the table is small&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Index Scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Index used, then table fetch&lt;/td&gt;
&lt;td&gt;Good for selective queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Index Only Scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Covering index, no table access&lt;/td&gt;
&lt;td&gt;Best case for that query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bitmap Index Scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple indexes combined&lt;/td&gt;
&lt;td&gt;Common for AND/OR conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Nested Loop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Join: for each row in A, scan B&lt;/td&gt;
&lt;td&gt;OK for small A, bad for large A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Hash Join&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build hash table on smaller table&lt;/td&gt;
&lt;td&gt;Standard for equi-joins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Explicit sort step&lt;/td&gt;
&lt;td&gt;Add an index on ORDER BY columns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;rows=&lt;/code&gt; estimate is critical.&lt;/strong&gt; If the planner estimates 1 row but the query actually returns 100,000, it may have chosen a nested loop when a hash join would have been faster. Run &lt;code&gt;ANALYZE&lt;/code&gt; after bulk loads to update statistics — stale statistics are the most common cause of bad plans.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Indexes Don't Help
&lt;/h2&gt;

&lt;p&gt;Indexes are not a performance panacea. They fail to help — or actively hurt — in several common scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low cardinality columns.&lt;/strong&gt; An index on a boolean column (2 distinct values) is almost never used. The planner would have to read half the table via index lookups, which is more expensive than a sequential scan. The same applies to any column where selectivity exceeds roughly 10-20%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functions on indexed columns.&lt;/strong&gt; &lt;code&gt;WHERE LOWER(email) = 'a@b.com'&lt;/code&gt; cannot use a regular index on &lt;code&gt;email&lt;/code&gt; because the index stores the original values, not the lowercased versions. The fix is a functional index: &lt;code&gt;CREATE INDEX ON users (LOWER(email))&lt;/code&gt;. Similarly, &lt;code&gt;WHERE date(created_at) = '2024-01-01'&lt;/code&gt; cannot use an index on &lt;code&gt;created_at&lt;/code&gt;; rewrite as &lt;code&gt;WHERE created_at &amp;gt;= '2024-01-01' AND created_at &amp;lt; '2024-01-02'&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implicit type casting.&lt;/strong&gt; &lt;code&gt;WHERE int_column = '42'&lt;/code&gt; (string literal) may prevent index use because the database must cast every row. Match types exactly: &lt;code&gt;WHERE int_column = 42&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leading wildcards.&lt;/strong&gt; &lt;code&gt;WHERE email LIKE '%@gmail.com'&lt;/code&gt; cannot use a B-tree index because the index is sorted from the left. &lt;code&gt;WHERE email LIKE 'john%'&lt;/code&gt; can use the index; &lt;code&gt;WHERE email LIKE '%john%'&lt;/code&gt; cannot. For substring search, consider full-text search indexes (GIN in PostgreSQL) instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too many indexes.&lt;/strong&gt; Each index slows writes. A table with 10 indexes takes roughly 10× longer to insert into than a table with none. If your workload is write-heavy (logging, event tracking), be stingy with indexes. If it's read-heavy (analytics, dashboards), be generous.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Worked Example: The Slow Dashboard
&lt;/h2&gt;

&lt;p&gt;A team reports that their admin dashboard takes 8 seconds to load. The query:&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="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
  &lt;span class="k"&gt;AND&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;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&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;id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;users&lt;/code&gt; table has 5 million rows; &lt;code&gt;orders&lt;/code&gt; has 50 million. &lt;code&gt;EXPLAIN&lt;/code&gt; shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sort (cost=452310.00..452310.50 rows=20 width=52)
  Sort Key: (count(o.id)) DESC
  -&amp;gt; HashAggregate (cost=452300.00..452305.00 rows=20 width=52)
        Group Key: u.id
        -&amp;gt; Hash Join (cost=125000.00..327300.00 rows=2500000 width=44)
              Hash Cond: (o.user_id = u.id)
              -&amp;gt; Seq Scan on orders (cost=0.00..150000.00 rows=10000000 width=12)
                    Filter: (total &amp;gt; 100)
              -&amp;gt; Hash (cost=100000.00..100000.00 rows=2500000 width=36)
                    -&amp;gt; Seq Scan on users (cost=0.00..100000.00 rows=2500000 width=36)
                          Filter: ((status = 'active') AND (created_at &amp;gt; '2024-01-01'))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tables are being fully scanned. The plan reads 2.5 million users and 10 million orders, joins them, groups, sorts, then returns 20 rows. The fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;CREATE INDEX ON users (status, created_at) INCLUDE (name, email);&lt;/code&gt; — covering index for the user filter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE INDEX ON orders (user_id) WHERE total &amp;gt; 100;&lt;/code&gt; — partial index for the order filter&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After indexing, &lt;code&gt;EXPLAIN&lt;/code&gt; shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limit (cost=0.85..1250.50 rows=20 width=52)
  -&amp;gt; Sort (cost=0.85..1250.50 rows=5000 width=52)
        Sort Key: (count(o.id)) DESC
        -&amp;gt; GroupAggregate (cost=0.85..1000.00 rows=5000 width=52)
              Group Key: u.id
              -&amp;gt; Nested Loop (cost=0.85..800.00 rows=5000 width=44)
                    -&amp;gt; Index Only Scan using users_status_created_at_idx on users
                          Index Cond: ((status = 'active') AND (created_at &amp;gt; '2024-01-01'))
                    -&amp;gt; Index Scan on orders_user_id_idx on orders
                          Index Cond: (user_id = u.id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cost dropped from 452,310 to 1,250 — a 360× improvement. The dashboard loads in 20ms instead of 8 seconds. The key insight: the planner now reads only the ~5,000 active recent users (via the covering index) and joins only their orders (via the partial index), instead of scanning both tables entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Tips for Database Performance
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Index columns in &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;ORDER BY&lt;/code&gt; clauses.&lt;/strong&gt; These are the columns the planner needs to filter, join, or sort on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the most selective column first in composite indexes.&lt;/strong&gt; The column that eliminates the most rows should be leftmost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use covering indexes for hot queries.&lt;/strong&gt; If a query runs thousands of times per minute, the extra storage is worth the speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;ANALYZE&lt;/code&gt; after bulk loads.&lt;/strong&gt; Stale statistics cause bad plans. PostgreSQL auto-analyzes, but large bulk loads may need a manual &lt;code&gt;ANALYZE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use partial indexes for filtered queries.&lt;/strong&gt; &lt;code&gt;CREATE INDEX ... WHERE total &amp;gt; 100&lt;/code&gt; is smaller and faster than indexing the whole table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't index everything.&lt;/strong&gt; Each index slows writes. A table with 10 indexes takes roughly 10× longer to insert into than a table with none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;EXPLAIN&lt;/code&gt; before deploying.&lt;/strong&gt; If you see &lt;code&gt;Seq Scan&lt;/code&gt; on a large table, investigate. The planner is telling you it has no better option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check selectivity before creating an index.&lt;/strong&gt; If the column has fewer than ~100 distinct values on a large table, the index will likely be ignored. The &lt;a href="https://notacalculator.com/calculator/sql-index-selectivity-calculator" rel="noopener noreferrer"&gt;SQL Index Selectivity Calculator&lt;/a&gt; tells you whether the planner will use it.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Limitations and When to Go Beyond Indexing
&lt;/h2&gt;

&lt;p&gt;Indexing is necessary but not sufficient for database performance at scale. When indexes alone aren't enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query rewriting.&lt;/strong&gt; A query that uses a subquery may be slower than one that uses a join, even with identical indexes. &lt;code&gt;EXPLAIN&lt;/code&gt; reveals the difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denormalization.&lt;/strong&gt; For read-heavy analytics, duplicating data (storing &lt;code&gt;order_count&lt;/code&gt; on the user row instead of computing it) trades write complexity for read speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partitioning.&lt;/strong&gt; Splitting a 500-million-row table into partitions by date lets the planner skip entire partitions — a 100× reduction in data scanned for time-bounded queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read replicas.&lt;/strong&gt; For workloads with many more reads than writes, replicas distribute the read load across multiple servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; Redis or application-level caching avoids the database entirely for frequently accessed, rarely changed data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Indexing is the first line of defense, not the last. But it is the one that catches the most common performance problems — and the one that most developers underuse because they've never learned to read the planner's output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a database index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A separate data structure (usually a B-tree) that the database maintains alongside your table. It allows the database to find rows without scanning the entire table, similar to an index at the back of a textbook.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why isn't my index being used?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Most often because selectivity is too high — the filter keeps too many rows (roughly more than 10-20% of the table), so the planner decides a sequential scan is cheaper. Other causes: functions on the indexed column (WHERE LOWER(col) = ...), implicit type casting, or stale statistics.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is selectivity?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The fraction of rows a filter keeps: rows returned divided by total rows. Low selectivity (few rows) means the index is useful. High selectivity (many rows) means the planner will likely ignore the index.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a composite index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;An index on two or more columns. Order matters: a composite index on (A, B) can answer queries filtering on A alone or A and B, but not B alone. Put the most selective column first.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a covering index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;An index that contains all the columns a query needs, so the database can answer the query from the index alone without fetching table rows. This is the fastest access path for a query.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I read an EXPLAIN plan?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Look for Seq Scan (full table scan — bad on large tables), Index Scan (index used — good), and Index Only Scan (covering index — best). The cost number lets you compare strategies; the rows estimate tells you how accurate the planner's guess is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does every column need an index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;No. Each index slows down INSERT, UPDATE, and DELETE operations. Index columns used in WHERE, JOIN, and ORDER BY clauses, and be skeptical of columns with fewer than ~100 distinct values on large tables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a partial index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;An index with a WHERE clause: CREATE INDEX ... WHERE total &amp;gt; 100. It indexes only the rows matching the condition, making it smaller and faster than a full-table index. Useful for queries that always filter on the same condition.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/database-performance-for-developers" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>architecture</category>
      <category>database</category>
      <category>learning</category>
    </item>
    <item>
      <title>Security Fundamentals for Web Developers: Authentication, Authorization, and the Attacks You Need to Prevent</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Thu, 27 Aug 2026 14:37:19 +0000</pubDate>
      <link>https://dev.to/apeder/security-fundamentals-for-web-developers-authentication-authorization-and-the-attacks-you-need-4a56</link>
      <guid>https://dev.to/apeder/security-fundamentals-for-web-developers-authentication-authorization-and-the-attacks-you-need-4a56</guid>
      <description>&lt;p&gt;&lt;em&gt;Authentication vs authorization, JWT deep dive, OAuth 2.0, password hashing, XSS/CSRF/SQL injection prevention, and security headers.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Developer Who Stored Passwords in Plaintext
&lt;/h2&gt;

&lt;p&gt;In 2012, LinkedIn suffered a breach that exposed 6.5 million password hashes. The hashes were unsalted SHA-1 — a hashing algorithm so fast that an attacker with a modern GPU could crack billions of hashes per hour. Within days, 60% of the passwords were recovered in plaintext. The attackers didn't need sophisticated exploits; they just needed a database dump and a rainbow table.&lt;/p&gt;

&lt;p&gt;The breach was preventable. Salting (adding random data to each password before hashing) and using a slow hashing algorithm (bcrypt, scrypt, or Argon2) would have made cracking infeasible. These weren't obscure techniques — they were well-documented best practices that LinkedIn's engineering team either didn't know or didn't prioritize. The cost: a $1.25 million settlement, a decade of reputational damage, and millions of users whose credentials were compromised.&lt;/p&gt;

&lt;p&gt;This is the uncomfortable truth about web security: &lt;strong&gt;most breaches aren't caused by sophisticated attacks. They're caused by developers who didn't know the basics.&lt;/strong&gt; SQL injection, cross-site scripting (XSS), and broken authentication have been on the OWASP Top 10 for over 20 years — and they're still the most common vulnerabilities in production applications.&lt;/p&gt;

&lt;p&gt;This guide covers the security fundamentals that every web developer should know: the difference between authentication and authorization, how JWTs actually work (and how they're misused), how to hash passwords correctly, how to prevent the OWASP Top 10 vulnerabilities, and which security headers to include in every response. The goal isn't to make you a security engineer — it's to make you a developer who doesn't ship the vulnerabilities that attackers are counting on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authentication vs Authorization: Knowing Who vs Knowing What They Can Do
&lt;/h2&gt;

&lt;p&gt;These two terms are confused so often that many developers use them interchangeably. They are fundamentally different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; answers: &lt;em&gt;"Who are you?"&lt;/em&gt; — verifying identity (username/password, biometrics, API key)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt; answers: &lt;em&gt;"Are you allowed to do this?"&lt;/em&gt; — checking permissions (is this user an admin? does this token have the &lt;code&gt;read:users&lt;/code&gt; scope?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A user who logs in with correct credentials is &lt;strong&gt;authenticated&lt;/strong&gt;. Whether they can delete other users' accounts is an &lt;strong&gt;authorization&lt;/strong&gt; decision. Confusing the two leads to vulnerabilities: a user who is authenticated but not authorized to access an admin endpoint can still access it if you only check "is logged in?".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The principle of least privilege:&lt;/strong&gt; every user, service, and token should have exactly the permissions it needs — no more. A background job that only reads from a database should not have write access. A frontend API token should not have admin scopes. A user who can view their own profile should not be able to view others'.&lt;/p&gt;




&lt;h2&gt;
  
  
  JWT Deep Dive: What's Inside the Token
&lt;/h2&gt;

&lt;p&gt;JSON Web Tokens (JWTs) are the most common mechanism for stateless authentication in modern web applications. A JWT is a self-contained token that encodes claims (user ID, permissions, expiration) in a JSON payload, signed by the server. The client sends the token in the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header, and the server verifies the signature to trust the claims.&lt;/p&gt;

&lt;p&gt;A JWT has three parts, each Base64URL-encoded and separated by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNjg3MzA1NjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt; (&lt;code&gt;eyJhbGciOiJIUzI1NiJ9&lt;/code&gt;): algorithm (HS256) and token type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt; (&lt;code&gt;eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNjg3MzA1NjAwfQ&lt;/code&gt;): claims — user_id, expiration, roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt; (&lt;code&gt;SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&lt;/code&gt;): HMAC of header+payload with server secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JWT ≠ encryption.&lt;/strong&gt; The payload is signed, not encrypted — anyone who intercepts the token can read the claims (that's why you should never put secrets in a JWT). The signature only guarantees that the token was issued by someone with the server secret and hasn't been tampered with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The critical claim is &lt;code&gt;exp&lt;/code&gt; (expiration).&lt;/strong&gt; A JWT without an expiration is valid forever — if leaked, it's a permanent access token. Set short expirations (15 minutes to 1 hour) and use refresh tokens for longer sessions. The &lt;a href="https://notacalculator.com/calculator/jwt-decoder" rel="noopener noreferrer"&gt;JWT Decoder&lt;/a&gt; parses tokens client-side, showing the header, payload, and signature — useful for debugging authentication flows and verifying that your tokens contain the expected claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common JWT mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storing sensitive data in the payload.&lt;/strong&gt; JWTs are signed, not encrypted. Anyone can read them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not validating the signature on the server.&lt;/strong&gt; If you trust the payload without verifying the signature, anyone can forge tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;alg: "none"&lt;/code&gt;.&lt;/strong&gt; Some JWT libraries accept tokens with no algorithm, bypassing signature verification entirely. Always pin the allowed algorithms server-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long expirations without refresh.&lt;/strong&gt; A token that's valid for 30 days is a 30-day window if leaked.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Password Hashing: Why Plaintext Is a Crime
&lt;/h2&gt;

&lt;p&gt;Storing passwords in plaintext is the most basic security mistake — and it still happens. When a database is breached (and databases are breached), plaintext passwords are immediately usable. The fix is &lt;strong&gt;hashing&lt;/strong&gt; : a one-way function that converts a password into a fixed-length string that cannot be reversed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not all hashing is equal.&lt;/strong&gt; Fast hash functions (MD5, SHA-1, SHA-256) are designed for speed — which is exactly what you don't want for passwords. An attacker with a modern GPU can compute billions of SHA-256 hashes per second. A slow hash function (bcrypt, scrypt, Argon2) is designed to be computationally expensive, making brute-force attacks infeasible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The algorithm comparison:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Hashes/second (GPU)&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MD5&lt;/td&gt;
&lt;td&gt;~180 billion&lt;/td&gt;
&lt;td&gt;❌ Never&lt;/td&gt;
&lt;td&gt;Broken, too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-1&lt;/td&gt;
&lt;td&gt;~6 billion&lt;/td&gt;
&lt;td&gt;❌ Never&lt;/td&gt;
&lt;td&gt;Broken, too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;td&gt;~2 billion&lt;/td&gt;
&lt;td&gt;❌ Never&lt;/td&gt;
&lt;td&gt;Too fast for passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt&lt;/td&gt;
&lt;td&gt;~100,000&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Adaptive cost factor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;~10,000&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Memory-hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2&lt;/td&gt;
&lt;td&gt;~1,000&lt;/td&gt;
&lt;td&gt;✅ Yes (best)&lt;/td&gt;
&lt;td&gt;Memory-hard, winner of Password Hashing Competition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Salting&lt;/strong&gt; adds random data to each password before hashing, so two users with the same password have different hashes. This defeats rainbow tables (precomputed hash databases). Modern algorithms (bcrypt, Argon2) handle salting automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;a href="https://notacalculator.com/calculator/password-generator" rel="noopener noreferrer"&gt;Password Generator&lt;/a&gt;&lt;/strong&gt; creates strong random passwords with configurable length and character sets — useful for generating initial passwords, API keys, or test data. But remember: the generator creates the password; bcrypt/Argon2 protects it in storage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The OWASP Top 10: Attacks Every Developer Should Prevent
&lt;/h2&gt;

&lt;p&gt;The OWASP Top 10 lists the most critical web application security risks. The ones every developer should know how to prevent:&lt;/p&gt;

&lt;h3&gt;
  
  
  Broken Object-Level Authorization (BOLA)
&lt;/h3&gt;

&lt;p&gt;The most common API vulnerability. If &lt;code&gt;GET /users/42&lt;/code&gt; returns user 42's data, but the server doesn't check that the authenticated user is user 42 (or an admin), then any authenticated user can access any other user's data by changing the ID in the URL. Every endpoint that takes an ID must check authorization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Always verify that the authenticated user is authorized to access the specific resource they're requesting. Don't rely on clients to only request their own data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Site Scripting (XSS)
&lt;/h3&gt;

&lt;p&gt;An attacker injects malicious JavaScript into a page that other users view. The script runs in the victim's browser, stealing cookies, session tokens, or performing actions on their behalf. Three types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reflected XSS:&lt;/strong&gt; the malicious script is part of the URL (e.g., &lt;code&gt;?search=&amp;lt;script&amp;gt;...&amp;lt;/script&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored XSS:&lt;/strong&gt; the script is stored in the database (e.g., in a comment or post)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOM-based XSS:&lt;/strong&gt; the vulnerability is in client-side JavaScript that unsafely manipulates the DOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Escape all user input before rendering it in HTML. Use a framework that auto-escapes by default (React, Vue, Angular). Set the &lt;code&gt;Content-Security-Policy&lt;/code&gt; header to restrict which scripts can execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Site Request Forgery (CSRF)
&lt;/h3&gt;

&lt;p&gt;An attacker tricks a user's browser into making a request to a site where the user is authenticated. If a bank transfer is triggered by &lt;code&gt;POST /transfer?to=attacker&amp;amp;amount=1000&lt;/code&gt;, and the user visits a malicious page while logged in, the browser sends the request with the user's cookies — and the transfer happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use CSRF tokens — a unique, unpredictable value embedded in every form and verified on the server. Modern frameworks (Rails, Django, Spring) include CSRF protection by default. For APIs, require a custom header (e.g., &lt;code&gt;X-Requested-With&lt;/code&gt;) that cross-origin requests cannot set.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL Injection
&lt;/h3&gt;

&lt;p&gt;An attacker injects SQL code through user input. If a query is built by string concatenation — &lt;code&gt;SELECT * FROM users WHERE name = '&lt;/code&gt; + userInput + &lt;code&gt;'&lt;/code&gt; — an attacker can input &lt;code&gt;' OR '1'='1&lt;/code&gt; to bypass authentication, or &lt;code&gt;'; DROP TABLE users; --&lt;/code&gt; to destroy data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use parameterized queries (prepared statements) exclusively. Never concatenate user input into SQL. ORMs (Sequelize, SQLAlchemy, Entity Framework) use parameterized queries by default — but raw SQL queries are still vulnerable if not parameterized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Misconfiguration
&lt;/h3&gt;

&lt;p&gt;Default credentials, exposed error messages, unnecessary features enabled, missing security headers. The 2017 Equifax breach was caused by an unpatched Apache Struts vulnerability — a known exploit for which a patch had been available for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Harden your deployment: disable default accounts, remove unused features, keep dependencies updated, and include security headers in every response.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Headers: The Headers Every Response Should Include
&lt;/h2&gt;

&lt;p&gt;HTTP security headers tell the browser how to handle your content. They're free protection — one line of configuration each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Recommended value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Force HTTPS for all future requests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-age=31536000; includeSubDomains&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X-Content-Type-Options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevent MIME type sniffing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X-Frame-Options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevent clickjacking (your page in an iframe)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DENY&lt;/code&gt; or &lt;code&gt;SAMEORIGIN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Content-Security-Policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restrict which resources can load&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default-src 'self'; script-src 'self'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X-XSS-Protection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enable browser XSS filter (legacy)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1; mode=block&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Referrer-Policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Control how much referrer information is sent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevent caching of sensitive data&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-store&lt;/code&gt; for authenticated responses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The most important:&lt;/strong&gt; &lt;code&gt;Strict-Transport-Security&lt;/code&gt; (HSTS) ensures that browsers always use HTTPS for your domain, preventing SSL stripping attacks. Once set, a user's browser will refuse to connect via HTTP for the duration of &lt;code&gt;max-age&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Session Management: Stateful vs Stateless
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stateful sessions&lt;/strong&gt; store session data on the server (in memory, Redis, or a database) and send the client a session ID cookie. The server looks up the session on every request. Pros: easy to invalidate, can store large data. Cons: requires shared session storage in a multi-server setup, memory overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateless sessions&lt;/strong&gt; (JWTs) encode all session data in the token itself. The server doesn't store anything. Pros: no shared storage needed, works across services. Cons: cannot invalidate individual tokens before expiration, token size grows with claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hybrid approach&lt;/strong&gt; (most common in production): use a short-lived JWT (15-60 minutes) for stateless authentication, plus a long-lived refresh token (stored server-side) to issue new JWTs. This gives you the scalability of stateless tokens with the ability to revoke access via the refresh token.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Tips for Secure Development
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never roll your own crypto.&lt;/strong&gt; Use established libraries (bcrypt, Argon2, libsodium). Cryptography is easy to get wrong in ways that are invisible until you're breached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use HTTPS everywhere.&lt;/strong&gt; No exceptions. Even internal APIs should use TLS. Use Let's Encrypt for free certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate input on the server.&lt;/strong&gt; Client-side validation is for UX; server-side validation is for security. Never trust client input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep dependencies updated.&lt;/strong&gt; Use &lt;code&gt;npm audit&lt;/code&gt;, Snyk, or Dependabot to flag known vulnerabilities in your dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log security events.&lt;/strong&gt; Failed logins, permission denied errors, and unusual patterns should be logged and monitored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use environment variables for secrets.&lt;/strong&gt; Never commit API keys, database passwords, or JWT secrets to version control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement rate limiting on authentication endpoints.&lt;/strong&gt; Brute-force attacks try thousands of passwords per second. Rate limit to 5-10 attempts per minute per IP.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Limitations: What This Guide Doesn't Cover
&lt;/h2&gt;

&lt;p&gt;This guide covers the fundamentals — the vulnerabilities that every developer should prevent. It does not cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Penetration testing&lt;/strong&gt; — actively trying to break your own application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threat modeling&lt;/strong&gt; — systematically identifying attack vectors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance&lt;/strong&gt; — GDPR, HIPAA, PCI-DSS requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure security&lt;/strong&gt; — network segmentation, firewalls, intrusion detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptography engineering&lt;/strong&gt; — designing custom protocols (don't)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production applications handling sensitive data, hire a security professional. The fundamentals prevent the most common attacks, but determined attackers with resources require defense in depth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between authentication and authorization?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Authentication answers 'who are you?' (verifying identity). Authorization answers 'are you allowed to do this?' (checking permissions). A user can be authenticated (logged in) but not authorized (not an admin).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a JWT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A JSON Web Token is a self-contained token that encodes claims (user ID, permissions, expiration) in a JSON payload, signed by the server. JWTs are signed, not encrypted — never put secrets in them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How should I store passwords?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Hash them with a slow algorithm: bcrypt, scrypt, or Argon2. Never use fast hashes (MD5, SHA-1, SHA-256) for passwords. Never store plaintext.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the most common web vulnerability?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Broken Object-Level Authorization (BOLA) — every endpoint that takes an ID must check that the authenticated user is authorized to access that specific resource.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is XSS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Cross-Site Scripting — an attacker injects malicious JavaScript into a page that other users view. Fix by escaping all user input and setting Content-Security-Policy headers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is CSRF?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Cross-Site Request Forgery — an attacker tricks a user's browser into making a request to a site where they're authenticated. Fix with CSRF tokens and SameSite cookies.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is SQL injection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;An attacker injects SQL code through user input. Fix by using parameterized queries (prepared statements) exclusively — never concatenate user input into SQL.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What security headers should I include?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;At minimum: Strict-Transport-Security (force HTTPS), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Content-Security-Policy to restrict resource loading.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/security-fundamentals-web-developers" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>sql</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>A/B Testing &amp; Experimentation: How to Run Tests That Actually Teach You Something</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Thu, 27 Aug 2026 04:48:07 +0000</pubDate>
      <link>https://dev.to/apeder/ab-testing-experimentation-how-to-run-tests-that-actually-teach-you-something-4lo1</link>
      <guid>https://dev.to/apeder/ab-testing-experimentation-how-to-run-tests-that-actually-teach-you-something-4lo1</guid>
      <description>&lt;p&gt;&lt;em&gt;Hypothesis testing, p-values, confidence intervals, statistical power, and the traps that fool even experienced teams.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tasting Illusion
&lt;/h2&gt;

&lt;p&gt;In the 1970s, Pepsi ran a marketing campaign that seemed to prove something remarkable: in blind taste tests, more people preferred Pepsi over Coke. The campaign was a sensation — millions of consumers sipped from unmarked cups and pointed to the sweeter drink. Pepsi's stock rose. Coke panicked, reformulated, and launched "New Coke" in 1985, one of the most infamous product launches in history.&lt;/p&gt;

&lt;p&gt;The problem wasn't the taste. It was the test. A single sip from a small cup measures immediate sweetness preference, not which beverage someone drinks for decades. The blind test had statistical significance — the difference was real and repeatable — but it lacked practical significance. It measured the wrong thing.&lt;/p&gt;

&lt;p&gt;This is the central trap of A/B testing and experimentation: &lt;strong&gt;statistical significance does not mean business significance&lt;/strong&gt;. A test can prove, with 99% confidence, that a button color change increases clicks by 0.3% — a finding that is simultaneously true and useless. This guide explains how to design and interpret experiments that avoid that trap: how to set up hypotheses, calculate whether your sample is large enough, interpret p-values without fooling yourself, and distinguish signals that matter from noise that merely looks convincing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Logic of Experimentation
&lt;/h2&gt;

&lt;p&gt;Every controlled experiment follows the same skeleton, whether you're testing a new cancer drug, a website headline, or a pricing page. You start with a &lt;strong&gt;hypothesis&lt;/strong&gt; — a specific, testable prediction. You split your subjects randomly into two groups: the &lt;strong&gt;control&lt;/strong&gt; (which sees the current version) and the &lt;strong&gt;treatment&lt;/strong&gt; (which sees the change). You measure an outcome in both groups. Then you ask a single question: &lt;em&gt;is the difference I see larger than what random chance would typically produce?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last step is where statistics enters. The logic is counterintuitive on purpose: you begin by assuming the boring explanation (your change did nothing) and then check whether the data is incompatible with that assumption. This starting assumption is called the &lt;strong&gt;null hypothesis&lt;/strong&gt; , written 

&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
. The alternative — your actual prediction — is the &lt;strong&gt;alternative hypothesis&lt;/strong&gt; , written 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
.&lt;/p&gt;

&lt;p&gt;The engine that drives the answer is the &lt;strong&gt;p-value&lt;/strong&gt; : the probability of observing a difference at least as large as the one you measured, assuming the null hypothesis is true.&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;P&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;observed&amp;nbsp;difference&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;∣&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;H&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;&amp;nbsp;is&amp;nbsp;true&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;A p-value of 0.03 means: &lt;em&gt;if your change actually did nothing, you would still see a difference this large 3% of the time by pure chance.&lt;/em&gt; It does NOT mean there is a 97% chance your change works. This misinterpretation is so common that the American Statistical Association published a formal statement warning against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Mistakes You Can Make
&lt;/h2&gt;

&lt;p&gt;Whenever you draw conclusions from an experiment, you can go wrong in two distinct ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Probability&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type I (false positive)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You conclude the change works when it doesn't&lt;/td&gt;
&lt;td&gt;p-value (α)&lt;/td&gt;
&lt;td&gt;Ship a dud, waste engineering time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type II (false negative)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You conclude the change does nothing when it actually works&lt;/td&gt;
&lt;td&gt;β&lt;/td&gt;
&lt;td&gt;Miss a real improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The p-value threshold you choose — the &lt;strong&gt;significance level&lt;/strong&gt; , denoted α — is the Type I error rate you're willing to tolerate. The conventional 0.05 means "I accept a 5% chance of shipping something that does nothing." In high-stakes domains (medicine, aviation), researchers demand α = 0.001 or lower. In fast-moving consumer apps, teams sometimes accept 0.10 to move faster.&lt;/p&gt;

&lt;p&gt;The Type II error rate, β, is the flip side. If β = 0.20, you have a 20% chance of missing a real effect. The complement, 1 − β, is called &lt;strong&gt;statistical power&lt;/strong&gt; : the probability that your test will detect an effect if one truly exists. A test with 80% power and 5% significance is the standard minimum in most fields — and many published studies fall short of even that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power is the probability you will detect a real effect. Most teams ignore it — and run tests that were doomed to be inconclusive before the first user arrived.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Size: The Number That Decides Everything
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable truth about A/B testing: &lt;strong&gt;a test with too few users cannot detect anything useful, no matter how good the change is.&lt;/strong&gt; If you run a test on 50 users and see no significant result, you have learned nothing — the test may simply have been too small to detect the effect. This is an underpowered test, and underpowered tests produce more noise than signal.&lt;/p&gt;

&lt;p&gt;The required sample size depends on four numbers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline conversion rate&lt;/strong&gt; (e.g., 5% of visitors sign up)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum detectable effect&lt;/strong&gt; (e.g., you want to catch a 10% relative lift → from 5% to 5.5%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Significance level&lt;/strong&gt; α (typically 0.05)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power&lt;/strong&gt; 1 − β (typically 0.80)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The formula for a two-proportion z-test (the standard for conversion rate comparisons) is approximately:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;z&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;/2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;z&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;β&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where p is the pooled proportion, 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the absolute effect size, and 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;z&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;/2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
, 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;z&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;β&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 are the critical z-values (1.96 and 0.84 for α=0.05, power=0.80).&lt;/p&gt;

&lt;p&gt;For a baseline of 5% and a minimum detectable effect of 10% relative (0.5 percentage points absolute), this gives n ≈ 15,700 users per variation. For a 5% relative lift — a more realistic goal — you need roughly 10× more: ~157,000 users per variation. This is why small teams with modest traffic often conclude that A/B testing "doesn't work" — they were running experiments that needed ten times their monthly traffic to detect anything. The &lt;a href="https://notacalculator.com/calculator/sample-size-calculator" rel="noopener noreferrer"&gt;Sample Size Calculator&lt;/a&gt; computes this directly for your baseline and desired power, so you can plan your test duration before launching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6et98wixpgztk8hzn65i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6et98wixpgztk8hzn65i.png" alt="Users per variation" width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Users per variation needed for 80% power, α=0.05, two-sided test. Smaller effects and lower baselines explode the sample size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical rule of thumb:&lt;/strong&gt; If your site gets 1,000 visitors per day and you need 15,700 per variation, the test runs for ~31 days. If your test takes longer than 4 weeks, reconsider whether the minimum detectable effect is realistic — or whether A/B testing is the right tool for your traffic level.&lt;/p&gt;


&lt;h2&gt;
  
  
  Confidence Intervals: The Number That Tells the Whole Story
&lt;/h2&gt;

&lt;p&gt;A p-value tells you whether the effect is "significant." A &lt;strong&gt;confidence interval&lt;/strong&gt; tells you the range of plausible effect sizes — which is usually what you actually want to know.&lt;/p&gt;

&lt;p&gt;A 95% confidence interval of [+0.2%, +1.8%] for a conversion rate lift means: we are 95% confident the true effect lies between 0.2 and 1.8 percentage points. Crucially, this interval does NOT include zero — which is why the p-value is below 0.05. But it also tells you something the p-value hides: the effect could be as small as 0.2%, which may not justify the engineering cost.&lt;/p&gt;

&lt;p&gt;The formula for a confidence interval on the difference between two proportions:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;±&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;z&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;/2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;When the confidence interval includes zero, the result is not statistically significant. When it does not include zero, you have a significant result — and the interval's width tells you how precisely you've estimated the effect. A wide interval ([−0.1%, +2.1%]) means "something is happening, but we're not sure exactly what." A narrow interval [+0.8%, +1.2%] means "the effect is almost certainly in this tight range". If you have the observed counts from a finished test, the &lt;a href="https://notacalculator.com/calculator/confidence-interval-calculator" rel="noopener noreferrer"&gt;Confidence Interval Calculator&lt;/a&gt; returns the interval directly — useful for reporting results to stakeholders who want to know the range, not just the p-value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the confidence interval as your primary result, not the p-value.&lt;/strong&gt; It answers the business question ("how much lift can I expect?") rather than the statistical one ("can I reject the null?").&lt;/p&gt;




&lt;h2&gt;
  
  
  Chi-Square and t-Tests: Choosing the Right Tool
&lt;/h2&gt;

&lt;p&gt;Not all experiments compare conversion rates. Sometimes you're comparing averages (revenue per user, time on page, order value), and sometimes you're comparing distributions across categories (did the change shift users between plan tiers?). The right test depends on the data type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For conversion rates and proportions&lt;/strong&gt; (click/no click, signup/no signup): use a &lt;strong&gt;chi-square test&lt;/strong&gt; or a two-proportion z-test. The chi-square test checks whether the observed frequencies in your 2×2 table (control vs treatment × converted vs not) differ from what the null hypothesis predicts.&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;χ&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;E&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;O&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;E&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;O&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the observed count and 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;E&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the expected count under the null. The result is compared to a chi-square distribution with 1 degree of freedom to obtain a p-value. The &lt;a href="https://notacalculator.com/calculator/chi-square-calculator" rel="noopener noreferrer"&gt;Chi-Square Calculator&lt;/a&gt; does this computation from your observed counts, returning both the statistic and the p-value — useful when you have the raw numbers from a finished test and want to verify significance without building the table by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For continuous outcomes&lt;/strong&gt; (revenue, time, score): use a &lt;strong&gt;t-test&lt;/strong&gt;. The t-test compares the means of two groups while accounting for the spread (standard deviation) within each groups:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the sample mean, $s$ is the standard deviation, and $n$ is the sample size. The denominator is the standard error of the difference — a quantity that combines the uncertainty from both groups. The &lt;a href="https://notacalculator.com/calculator/t-test-calculator" rel="noopener noreferrer"&gt;t-Test Calculator&lt;/a&gt; computes this from your two samples, handling both the equal-variance and unequal-variance (Welch's) cases.&lt;/p&gt;

&lt;p&gt;The standard deviation matters enormously here. If revenue per user swings wildly (high variance), you need far more users to detect the same mean difference than if revenue is consistent. This is why the confidence interval on the mean difference is so useful: it shows whether the signal (the mean difference) is large relative to the noise (the standard deviation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical decision rule:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary outcome (clicked/didn't, signed up/didn't) → chi-square or z-test for proportions&lt;/li&gt;
&lt;li&gt;Continuous outcome (revenue, time, score) → t-test&lt;/li&gt;
&lt;li&gt;Comparing more than two variants → ANOVA (not covered here)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Traps That Fool Experienced Teams
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Peeking at Results
&lt;/h3&gt;

&lt;p&gt;The p-value formula assumes you look at the data exactly once, at a predetermined sample size. If you peek at your test on day 3, day 7, day 10 — and stop as soon as p &amp;lt; 0.05 — you are dramatically inflating your false positive rate. With 5 peeks, the true false positive rate is not 5% but roughly 14%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Decide your sample size in advance, run the test to that sample size, then analyze once. If you must peek, use &lt;strong&gt;sequential testing&lt;/strong&gt; methods (always-valid p-values, alpha spending) that adjust the threshold for each peek — but these require roughly 30% more users for the same power.&lt;/p&gt;

&lt;h3&gt;
  
  
  P-Hacking (Multiple Comparisons)
&lt;/h3&gt;

&lt;p&gt;If you test 20 different metrics (conversion rate, revenue, time on page, scroll depth, bounce rate...) and report the one that came back significant at p &amp;lt; 0.05, you are p-hacking. With 20 independent tests at α = 0.05, the probability of at least one false positive is 1 − (1−0.05)^20 ≈ 64%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pre-specify a single &lt;strong&gt;primary metric&lt;/strong&gt; before the test begins. Treat everything else as exploratory. If you must test multiple metrics, apply a Bonferroni correction: divide α by the number of tests (so 0.05 / 20 = 0.0025 for 20 metrics).&lt;/p&gt;

&lt;h3&gt;
  
  
  Simpson's Paradox
&lt;/h3&gt;

&lt;p&gt;A treatment can win in every subgroup (mobile users, desktop users, new visitors, returning visitors) and lose in aggregate — or vice versa. This happens when the subgroups have very different sizes and very different baseline rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Always segment your results. If the treatment effect reverses between mobile and desktop, the aggregate number is misleading. Report the per-segment results alongside the overall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Novelty and Primacy Effects
&lt;/h3&gt;

&lt;p&gt;A new feature may initially perform worse (users are confused) or better (users are curious) than it will long-term. A 2-week test captures the novelty effect, not the steady-state effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Run tests for at least 2 full weeks (to capture day-of-week effects) and ideally 4 weeks for features with learning curves. Be skeptical of results that appear in week 1 and vanish in week 2.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Worked Example: Button Color
&lt;/h2&gt;

&lt;p&gt;A team tests whether a green signup button outperforms the current blue one. Baseline conversion rate: 4.0%. They want to detect a 10% relative lift (to 4.4%) with 80% power and α = 0.05.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — sample size.&lt;/strong&gt; Using the formula above with p₁ = 0.04, p₂ = 0.044, α = 0.05, power = 0.80: n ≈ 39,000 users per variation. With 20,000 daily visitors split 50/50, the test runs for ~4 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — run the test.&lt;/strong&gt; Control (blue): 780 conversions out of 39,000 (2.00%). Treatment (green): 936 conversions out of 39,000 (2.40%).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — chi-square test.&lt;/strong&gt; The observed counts form a 2×2 table. The chi-square statistic is 15.2, which gives p ≈ 0.0001 — well below 0.05. The result is statistically significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — confidence interval.&lt;/strong&gt; The difference is 0.40 percentage points, with a 95% CI of [+0.22%, +0.58%]. This interval excludes zero (confirming significance) and tells the business: "the lift is somewhere between 0.22 and 0.58 percentage points, or 5.5% to 14.5% relative."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — decision.&lt;/strong&gt; Is a 0.4 percentage point lift worth the engineering cost and the design inconsistency? That is a business question, not a statistical one. The statistics only say: the lift is real and almost certainly between 0.22% and 0.58%.&lt;/p&gt;




&lt;h2&gt;
  
  
  When A/B Testing Is the Wrong Tool
&lt;/h2&gt;

&lt;p&gt;A/B testing is powerful, but it is not always the right method. Consider alternatives when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your traffic is too low.&lt;/strong&gt; If you need 100,000 users per variation and get 5,000 per month, a properly powered test would take years. Use qualitative research, user interviews, or heuristic evaluation instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're testing a fundamental redesign.&lt;/strong&gt; A/B testing measures incremental changes. A complete redesign has too many variables changing at once to attribute causality. Use before/after analysis with caution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network effects exist.&lt;/strong&gt; If users in the treatment group interact with users in the control group (social networks, marketplaces), the groups are no longer independent. The test is invalid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to understand WHY.&lt;/strong&gt; A/B testing tells you &lt;em&gt;whether&lt;/em&gt; something works, not &lt;em&gt;why&lt;/em&gt;. Pair experiments with qualitative research (session recordings, user interviews) to understand the mechanism.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Tips for Running Experiments
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-register your test.&lt;/strong&gt; Write down the hypothesis, primary metric, sample size, and analysis plan before collecting data. This prevents post-hoc rationalization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run an A/A test occasionally.&lt;/strong&gt; Show both groups the same experience. If you get a "significant" result, your testing infrastructure is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment after, not during.&lt;/strong&gt; Pre-specify one primary metric. Segment (by device, by region, by new/returning) only as exploratory analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track guardrail metrics.&lt;/strong&gt; A treatment might increase clicks but hurt revenue. Define guardrail metrics (revenue per user, unsubscribe rate, page load time) that must not degrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account for multiple variants.&lt;/strong&gt; If you run an A/B/C/D test, apply a Bonferroni correction (α / number of comparisons) or use a false discovery rate method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report confidence intervals, not just p-values.&lt;/strong&gt;"Lift of 0.4 pp, 95% CI [+0.22, +0.58]" is more actionable than "p = 0.0001."&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Limitations and Edge Cases
&lt;/h2&gt;

&lt;p&gt;A/B testing has honest limitations. It is &lt;strong&gt;not good at detecting small effects&lt;/strong&gt; unless you have enormous traffic. It &lt;strong&gt;assumes random assignment&lt;/strong&gt; — if your randomization is broken (e.g., more mobile users end up in treatment by chance), the results are biased. It &lt;strong&gt;measures short-term effects&lt;/strong&gt; — a headline that clicks well today may fatigue users after a month. And it &lt;strong&gt;cannot detect interactions&lt;/strong&gt; unless you specifically design for them (a treatment may help new users but hurt returning users, which cancels out in aggregate).&lt;/p&gt;

&lt;p&gt;Finally, A/B testing is a tool for optimization, not for strategy. It can tell you which of two headlines works better, but it cannot tell you what your content strategy should be. Use it to refine, not to think.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a p-value?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The probability of observing a result at least as extreme as yours, assuming the null hypothesis (that your change did nothing) is true. A p-value of 0.05 means: if your change did nothing, you would still see a result this large 5% of the time by chance. It is NOT the probability that your change works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between statistical significance and practical significance?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Statistical significance means the observed difference is unlikely due to chance (low p-value). Practical significance means the difference is large enough to matter for your business. A result can be statistically significant (real) but practically insignificant (too small to care about).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How many users do I need for an A/B test?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It depends on your baseline rate and the minimum effect you want to detect. For a 5% baseline and a 10% relative lift (to 5.5%), you need roughly 15,700 users per variation for 80% power at α=0.05. Smaller effects require dramatically more users.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is statistical power?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The probability that your test will detect a real effect if one exists. Typically set to 80% (β=0.20). An underpowered test — one with too few users — will often conclude 'no significant difference' even when a real effect exists.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why shouldn't I peek at my test results every day?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Because each peek inflates your false positive rate. With 5 peeks at α=0.05, your true false positive rate is roughly 14%. Decide your sample size in advance and analyze once, or use sequential testing methods that adjust for peeking.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between a t-test and a chi-square test?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Use a t-test when comparing means of continuous outcomes (revenue, time, score). Use a chi-square test when comparing proportions or frequencies (conversion rates, click/no-click). The t-test assumes continuous data; the chi-square test works with counts in categories.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a confidence interval?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A range of plausible values for the true effect. A 95% confidence interval of [+0.2%, +1.8%] means we are 95% confident the true lift is between 0.2 and 1.8 percentage points. If the interval excludes zero, the result is statistically significant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is Simpson's Paradox?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A phenomenon where a trend appears in every subgroup but reverses in aggregate (or vice versa). It happens when subgroups have very different sizes and baselines. Always segment your results — the aggregate can hide or reverse real effects.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/ab-testing-experimentation-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>productivity</category>
      <category>architecture</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Statistics for Data-Driven Developers: Making Decisions With Numbers</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Wed, 26 Aug 2026 16:37:47 +0000</pubDate>
      <link>https://dev.to/apeder/statistics-for-data-driven-developers-making-decisions-with-numbers-5fb9</link>
      <guid>https://dev.to/apeder/statistics-for-data-driven-developers-making-decisions-with-numbers-5fb9</guid>
      <description>&lt;p&gt;&lt;em&gt;Descriptive stats, probability distributions, confidence intervals, hypothesis testing, and the mistakes that fool experienced engineers.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Alert That Cried Wolf
&lt;/h2&gt;

&lt;p&gt;A team at a mid-size SaaS company set up monitoring for their API response times. They configured an alert: "Trigger if average response time exceeds 500 ms over a 5-minute window." The alert fired constantly. Engineers sprinted to investigate, found nothing wrong, and started ignoring it. Two weeks later, a genuine outage went undetected for 20 minutes because everyone had learned to dismiss the alert.&lt;/p&gt;

&lt;p&gt;The problem was the word "average." The average response time was 350 ms, but the distribution was heavily skewed: 95% of requests completed in under 200 ms, while 5% took over 2 seconds (timeouts and retries). The average hid the tail — the 5% of users having a terrible experience. When the team switched to alerting on the &lt;strong&gt;95th percentile&lt;/strong&gt; (p95) of response time instead of the mean, the false alarms stopped. The p95 was 450 ms — still healthy — and the tail latency that had been dragging up the average became visible in a separate dashboard.&lt;/p&gt;

&lt;p&gt;This is the fundamental lesson of statistics for developers: &lt;strong&gt;the summary statistic you choose determines the story you see.&lt;/strong&gt; Mean, median, and percentile each reveal different truths. Standard deviation tells you whether your system is consistent or erratic. Confidence intervals tell you whether a change is real or noise. Hypothesis testing tells you whether your "improvement" actually improved anything or whether you're just seeing random variation.&lt;/p&gt;

&lt;p&gt;This guide explains the statistics that matter for developers who work with data — whether that data is response times, conversion rates, error rates, or business metrics. It covers descriptive statistics (how to summarize data), probability (how to model uncertainty), and inferential statistics (how to draw conclusions from samples) — with the goal of making better decisions and avoiding the traps that fool even experienced engineers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Descriptive Statistics: Summarizing Without Lying
&lt;/h2&gt;

&lt;p&gt;Descriptive statistics compress a dataset into a few numbers. The art is choosing numbers that tell the truth — and not choosing numbers that hide it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Central Tendency: Mean, Median, Mode
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;mean&lt;/strong&gt; (average) is the most common summary — and the most misleading when data is skewed. If 9 users pay $10/month and 1 user pays $1,000/month, the mean revenue per user is $109, which describes no one's actual experience.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;median&lt;/strong&gt; (middle value) is robust to outliers. In the same dataset, the median is $10 — a more honest summary of the typical user. Use median when data is skewed (revenue, response times, house prices) and mean when data is symmetric (heights, test scores, measurement errors).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;mode&lt;/strong&gt; (most frequent value) matters for categorical data: the most common error code, the most popular feature, the most frequent user country.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spread: Variance and Standard Deviation
&lt;/h3&gt;

&lt;p&gt;Central tendency tells you where the middle is. &lt;strong&gt;Spread&lt;/strong&gt; tells you how far the data ranges around that middle. Two APIs can have the same mean response time (350 ms) but wildly different consistency: one with σ = 50 ms (predictable) and one with σ = 400 ms (erratic).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variance&lt;/strong&gt; is the average squared deviation from the mean:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mop op-symbol small-op"&gt;∑&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Standard deviation&lt;/strong&gt; is the square root of variance, bringing the units back to the original data:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;For the normal distribution, roughly 68% of values fall within ±1 standard deviation of the mean, 95% within ±2, and 99.7% within ±3 — the &lt;strong&gt;empirical rule&lt;/strong&gt;. The &lt;a href="https://notacalculator.com/calculator/standard-deviation-calculator" rel="noopener noreferrer"&gt;Standard Deviation Calculator&lt;/a&gt; and &lt;a href="https://notacalculator.com/calculator/variance-calculator" rel="noopener noreferrer"&gt;Variance Calculator&lt;/a&gt; compute these from your raw data, including the crucial sample-vs-population distinction (divide by n-1 for samples, n for populations).&lt;/p&gt;

&lt;h3&gt;
  
  
  Percentiles: The Tail Tells the Story
&lt;/h3&gt;

&lt;p&gt;For skewed data (response times, revenue, file sizes), percentiles are more honest than mean + standard deviation. The &lt;strong&gt;p50&lt;/strong&gt; is the median. The &lt;strong&gt;p95&lt;/strong&gt; is the value below which 95% of observations fall. The &lt;strong&gt;p99&lt;/strong&gt; captures the worst 1%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why percentiles matter for monitoring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mean&lt;/strong&gt; hides the tail (the alert that cried wolf)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p95&lt;/strong&gt; captures the experience of the slowest 5% without being dominated by outliers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99&lt;/strong&gt; captures the worst 1% — the users who are most likely to churn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Percentiles are not additive.&lt;/strong&gt; The p95 of a sum is not the sum of the p95s. If API A has p95 = 200 ms and API B has p95 = 300 ms, the end-to-end p95 of calling both is not 500 ms — it's typically less, because the slowest 5% of A's calls don't always coincide with the slowest 5% of B's calls. This is why distributed tracing matters: you need to measure the end-to-end percentile, not sum individual ones.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Response time percentiles for a typical API. The median (180ms) looks healthy, but p99 reveals 1% of users wait over 1 second. The mean (350ms) is pulled up by the tail.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Probability: Modeling Uncertainty
&lt;/h2&gt;

&lt;p&gt;Probability is the language of uncertainty. For developers, it appears in reliability engineering (what's the chance of outage?), capacity planning (what's the chance we exceed capacity?), and A/B testing (what's the chance this result is real?).&lt;/p&gt;

&lt;h3&gt;
  
  
  Distributions: The Shapes of Data
&lt;/h3&gt;

&lt;p&gt;Every dataset has a shape — a &lt;strong&gt;distribution&lt;/strong&gt;. The most important distributions for developers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normal (Gaussian)&lt;/strong&gt; distribution is the bell curve: symmetric, mean = median = mode. Heights, measurement errors, and sums of many independent variables tend to be normal (thanks to the Central Limit Theorem, discussed below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log-normal&lt;/strong&gt; distribution is skewed right: the log of the values is normal. Response times, file sizes, and revenue per user tend to be log-normal. The mean is much higher than the median. Use geometric mean or percentiles, not arithmetic mean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exponential&lt;/strong&gt; distribution models time between events: time between requests, time between failures. It has a "memoryless" property: the probability of a failure in the next minute is the same regardless of how long it's been since the last failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poisson&lt;/strong&gt; distribution models counts of events in a fixed interval: requests per second, errors per hour, support tickets per day. If events arrive independently at rate λ, the count in any interval is Poisson-distributed with mean λ.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Central Limit Theorem
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Central Limit Theorem (CLT)&lt;/strong&gt; is one of the most powerful results in statistics: if you take sufficiently large random samples from ANY distribution (normal, skewed, it doesn't matter), the distribution of the sample means will be approximately normal.&lt;/p&gt;

&lt;p&gt;This is why the normal distribution appears everywhere: any metric that's the sum or average of many independent contributions (response times averaged over a window, conversion rates averaged over days) tends toward normal, even if the underlying data is wildly skewed. The CLT is also why you can use normal-based methods (z-tests, t-tests) even when your data isn't normal — as long as your sample size is large enough (typically n &amp;gt; 30).&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlation: The Number That Gets Misused
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Correlation&lt;/strong&gt; measures the linear relationship between two variables, ranging from -1 (perfect inverse) to +1 (perfect direct). A correlation of 0 means no linear relationship.&lt;/p&gt;

&lt;p&gt;The Pearson correlation coefficient:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mop op-symbol small-op"&gt;∑&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mop op-symbol small-op"&gt;∑&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mop op-symbol small-op"&gt;∑&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;y&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Correlation ≠ causation&lt;/strong&gt; is the most violated principle in data analysis. Ice cream sales and drowning deaths are correlated — not because ice cream causes drowning, but because both increase in summer (a &lt;strong&gt;confounding variable&lt;/strong&gt; ). In software: server load and error rates are correlated, but the cause is a third factor (a deployment that introduced both a memory leak and increased CPU usage).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spurious correlations&lt;/strong&gt; appear by chance when you test enough pairs. If you check 100 unrelated metrics, about 5 will show "significant" correlation at p &amp;lt; 0.05 purely by chance. This is why pre-specifying hypotheses matters — and why data mining without theory produces nonsense.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inferential Statistics: Drawing Conclusions from Samples
&lt;/h2&gt;

&lt;p&gt;Descriptive statistics summarize what you have. &lt;strong&gt;Inferential statistics&lt;/strong&gt; let you draw conclusions about a population from a sample — which is essential when you can't measure everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confidence Intervals: The Range of Plausible Values
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;confidence interval&lt;/strong&gt; is a range that likely contains the true population parameter. A 95% confidence interval of [2.1%, 2.5%] for a conversion rate means: "if we repeated this experiment many times, 95% of the intervals we'd compute would contain the true conversion rate".&lt;/p&gt;

&lt;p&gt;The formula for a confidence interval on a mean:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="accent-body"&gt;&lt;span class="mord"&gt;ˉ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;±&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;z&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;/2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;The key insight: the width shrinks with 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord sqrt mtight"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail mtight"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 — to halve the margin of error, you need 4× more data. The &lt;a href="https://notacalculator.com/calculator/confidence-interval-calculator" rel="noopener noreferrer"&gt;Confidence Interval Calculator&lt;/a&gt; computes this from your sample mean, standard deviation, and sample size, for any confidence level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why confidence intervals beat point estimates.&lt;/strong&gt; Reporting "conversion rate is 2.3%" hides the uncertainty. Reporting "conversion rate is 2.3% ± 0.2% (95% CI)" tells you the precision of your estimate. If the CI for the difference between two variants includes zero, the difference is not statistically significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hypothesis Testing: Is This Real or Noise?
&lt;/h3&gt;

&lt;p&gt;Hypothesis testing is the formal framework for deciding whether an observed effect is real or due to chance. The logic: assume the boring explanation (null hypothesis), compute the probability of seeing your data under that assumption (p-value), and reject the null if that probability is low enough.&lt;/p&gt;

&lt;p&gt;The framework:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Null hypothesis (H₀):&lt;/strong&gt; the default assumption (e.g., "the new feature does nothing")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alternative hypothesis (H₁):&lt;/strong&gt; what you want to prove (e.g., "the new feature increases conversion")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test statistic:&lt;/strong&gt; a number computed from your data (t-statistic, chi-square statistic, z-score)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p-value:&lt;/strong&gt; the probability of seeing a test statistic at least as extreme as yours, assuming H₀ is true&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision:&lt;/strong&gt; if p &amp;lt; α (typically 0.05), reject H₀&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The p-value is not the probability that H₀ is true.&lt;/strong&gt; It is the probability of the data given H₀ — not the probability of H₀ given the data. This confusion is so pervasive that the American Statistical Association published a formal statement clarifying it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/t-test-calculator" rel="noopener noreferrer"&gt;t-Test Calculator&lt;/a&gt; and &lt;a href="https://notacalculator.com/calculator/chi-square-calculator" rel="noopener noreferrer"&gt;Chi-Square Calculator&lt;/a&gt; compute test statistics and p-values for the two most common tests: t-test for comparing means (revenue, time, score) and chi-square for comparing proportions (conversion rates, click/no-click).&lt;/p&gt;

&lt;h3&gt;
  
  
  Statistical Power: The Probability You'll Detect a Real Effect
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Statistical power&lt;/strong&gt; is the probability that your test will detect an effect if one truly exists. A test with 80% power has a 20% chance of missing a real effect (Type II error).&lt;/p&gt;

&lt;p&gt;Power depends on three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Effect size:&lt;/strong&gt; larger effects are easier to detect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample size:&lt;/strong&gt; more data → more power&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Significance level:&lt;/strong&gt; lower α → less power (stricter threshold)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/sample-size-calculator" rel="noopener noreferrer"&gt;Sample Size Calculator&lt;/a&gt; computes how many observations you need for a desired power, given your expected effect size and α. Use it before running any experiment — an underpowered test is worse than no test at all, because it produces false confidence in a "no difference" result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Applications for Developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monitoring and Alerting
&lt;/h3&gt;

&lt;p&gt;The most common statistical mistake in monitoring is alerting on the mean. For skewed distributions (response times, queue lengths, file sizes), the mean hides the tail. Alert on percentiles instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;p95&lt;/strong&gt; for "most users are having a good experience"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99&lt;/strong&gt; for "the worst 1% are having a bad experience"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mean&lt;/strong&gt; only for symmetric metrics (CPU utilization, memory usage).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Set thresholds using baselines, not absolutes.&lt;/strong&gt; A response time of 500 ms might be normal for a database query but catastrophic for a cache lookup. Establish per-endpoint baselines using historical percentiles, and alert on deviations from the baseline (e.g., "p95 is 3× higher than the 7-day rolling average").&lt;/p&gt;

&lt;h3&gt;
  
  
  A/B Testing and Experimentation
&lt;/h3&gt;

&lt;p&gt;A/B testing is hypothesis testing applied to product decisions. The same principles apply: pre-specify your primary metric, compute sample size in advance, run the test to completion, and report confidence intervals — not just p-values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Peeking:&lt;/strong&gt; checking results daily and stopping when p &amp;lt; 0.05 inflates false positives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple comparisons:&lt;/strong&gt; testing 20 metrics and reporting the one that's significant is p-hacking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underpowered tests:&lt;/strong&gt; running on too few users produces inconclusive results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring practical significance:&lt;/strong&gt; a 0.1% lift may be statistically significant with 1M users but not worth shipping&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data-Driven Development
&lt;/h3&gt;

&lt;p&gt;"Data-driven" means letting evidence guide decisions — but evidence can be misleading if you don't understand the statistics. Before trusting any metric:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the distribution.&lt;/strong&gt; Is it normal or skewed? Use mean for normal, median/percentiles for skewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the spread.&lt;/strong&gt; A mean of 350 ms with σ = 50 ms is very different from a mean of 350 ms with σ = 400 ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute confidence intervals.&lt;/strong&gt; A conversion rate of "2.3%" is less useful than "2.3% ± 0.2%."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beware of confounding.&lt;/strong&gt; Correlation is not causation. A metric may move because of a third factor you're not measuring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-specify hypotheses.&lt;/strong&gt; Decide what you're testing before you look at the data. Post-hoc rationalization produces false discoveries.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Capacity Planning and Forecasting
&lt;/h3&gt;

&lt;p&gt;Statistics also powers capacity planning: predicting how much infrastructure you'll need. If your traffic grows 10% per month, you can extrapolate when you'll need to scale. But extrapolation requires understanding variance — if traffic is highly variable (high standard deviation), you need more headroom than the average suggests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; plan for the p95 of expected traffic, not the mean. If your mean projected traffic is 10,000 requests per second, but the 95th percentile is 15,000, provision for 15,000. Otherwise, you'll be overloaded 5% of the time — which is roughly 3.6 hours per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regression for forecasting.&lt;/strong&gt; Simple linear regression fits a line to historical data (traffic over time) and extrapolates. The regression calculator on this site fits a line to your data and reports the slope (growth rate) and R² (how well the line fits). Use it to answer: "At current growth rate, when will we hit our limit?" But be cautious — regression assumes the future resembles the past. A viral post, a product launch, or a pandemic breaks that assumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Budgets and Reliability
&lt;/h3&gt;

&lt;p&gt;Site Reliability Engineering (SRE) uses statistics to define &lt;strong&gt;error budgets&lt;/strong&gt;. If your SLO (Service Level Objective) is 99.9% uptime, your error budget is 0.1% — roughly 43 minutes of downtime per month. Statistics tells you whether you're on track to meet that budget or whether you've already blown it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The math:&lt;/strong&gt; if you've had 30 minutes of downtime in the first 15 days of a 30-day month, you're on pace for 60 minutes — double your budget. But if your downtime events are random (Poisson-distributed), the variance matters. A month with 30 minutes of downtime could be normal variation or the start of a trend. Statistical process control charts (which plot metrics with control limits at ±3σ) distinguish between normal variation and a genuine shift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes That Fool Experienced Engineers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confusing correlation with causation.&lt;/strong&gt; Two metrics moving together doesn't mean one causes the other. Look for confounding variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring sample size.&lt;/strong&gt; A 5% conversion rate from 20 users (1 conversion) is not a reliable estimate. Compute confidence intervals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using mean for skewed data.&lt;/strong&gt; Response times, revenue, and file sizes are almost always skewed. Use percentiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not checking assumptions.&lt;/strong&gt; t-tests assume approximately normal data (or large samples). Chi-square tests assume sufficient expected counts. Violating assumptions produces unreliable p-values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating p &amp;lt; 0.05 as truth.&lt;/strong&gt; A p-value of 0.04 means there's a 4% chance of this result under the null — not that the alternative is true. Replicate findings before acting on them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting about practical significance.&lt;/strong&gt; With enough data, any tiny effect becomes statistically significant. Ask: "Is this effect large enough to matter?"&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between mean and median?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Mean is the average (sum divided by count). Median is the middle value when data is sorted. Mean is sensitive to outliers; median is robust. Use mean for symmetric data, median for skewed data (response times, revenue).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is standard deviation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A measure of spread: how far data typically sits from the mean. A small standard deviation means data is tightly clustered; a large one means data is widely scattered. For normal data, 68% falls within ±1σ, 95% within ±2σ.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a percentile?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The value below which a given percentage of observations fall. p50 is the median. p95 is the value below which 95% of observations fall. Percentiles are robust to outliers and ideal for skewed data like response times.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a confidence interval?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A range that likely contains the true population parameter. A 95% CI of [2.1%, 2.5%] means: if we repeated this experiment many times, 95% of the intervals would contain the true value. If the CI for a difference includes zero, the result is not statistically significant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a p-value?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The probability of observing a result at least as extreme as yours, assuming the null hypothesis (no effect) is true. A p-value of 0.05 means: if there were no real effect, you'd see a result this large 5% of the time by chance. It is NOT the probability that your hypothesis is true.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is statistical power?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The probability that your test will detect a real effect if one exists. Typically set to 80% (β=0.20). An underpowered test — one with too few users — will often conclude 'no significant difference' even when a real effect exists.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the Central Limit Theorem?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;The principle that the distribution of sample means approaches a normal distribution as sample size increases, regardless of the underlying data distribution. This is why normal-based methods work even with non-normal data — as long as your sample is large enough (typically n &amp;gt; 30).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between correlation and causation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Correlation means two variables move together. Causation means one causes the other. Correlation does not imply causation — both variables may be influenced by a third confounding factor. Ice cream sales and drowning are correlated (both increase in summer) but not causally related.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/statistics-for-developers" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>performance</category>
      <category>backend</category>
      <category>datascience</category>
    </item>
    <item>
      <title>API Design &amp; Rate Limiting: Building APIs That Scale Without Breaking</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Wed, 26 Aug 2026 14:34:20 +0000</pubDate>
      <link>https://dev.to/apeder/api-design-rate-limiting-building-apis-that-scale-without-breaking-2662</link>
      <guid>https://dev.to/apeder/api-design-rate-limiting-building-apis-that-scale-without-breaking-2662</guid>
      <description>&lt;p&gt;&lt;em&gt;How to design REST APIs that scale: HTTP methods, status codes, pagination, rate limiting (token bucket, sliding window), JWT auth, and security best practices.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The API That Became a Weapon
&lt;/h2&gt;

&lt;p&gt;In 2021, a developer at a major cloud provider wrote a blog post about an outage that cost the company an estimated $10 million in lost revenue. The root cause wasn't a hardware failure or a DDoS attack. It was a single internal API that was designed without rate limits. A misconfigured internal service began calling the API in a retry loop — 10,000 requests per second, each timing out and retrying immediately. Within minutes, the API was down. Cascading failures took out three dependent services. By the time engineers identified the source, customers had been unable to access their data for 4 hours.&lt;/p&gt;

&lt;p&gt;The API had no rate limiting, no circuit breaker, and no &lt;code&gt;Retry-After&lt;/code&gt; header to tell the client to back off. It was, in retrospect, an accident waiting to happen — and it happened.&lt;/p&gt;

&lt;p&gt;This guide explains how to design APIs that scale gracefully: how to use HTTP methods and status codes correctly, how to implement rate limiting that protects your infrastructure without punishing legitimate users, how to handle authentication with JWTs, and how to avoid the security mistakes that turn APIs into attack surfaces. Whether you're building your first REST API or hardening a production service that handles millions of requests, the same principles apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  REST API Design: Resources, Not Actions
&lt;/h2&gt;

&lt;p&gt;The most common mistake in API design is treating endpoints like function calls. A poorly designed API looks like a list of actions: &lt;code&gt;/getUser&lt;/code&gt;, &lt;code&gt;/createOrder&lt;/code&gt;, &lt;code&gt;/deleteProduct&lt;/code&gt;. A well-designed API looks like a map of resources: &lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/orders&lt;/code&gt;, &lt;code&gt;/products&lt;/code&gt;. The difference matters because HTTP already provides the verbs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; (Representational State Transfer) models your application as a set of resources identified by URLs, and uses HTTP methods to perform operations on those resources. The four methods you'll use 95% of the time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Idempotent&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve a resource&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /users/42&lt;/code&gt; returns user 42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new resource&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /users&lt;/code&gt; creates a new user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replace a resource entirely&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PUT /users/42&lt;/code&gt; replaces user 42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update part of a resource&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PATCH /users/42&lt;/code&gt; updates some fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove a resource&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DELETE /users/42&lt;/code&gt; removes user 42&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt; means making the same request multiple times produces the same result as making it once. &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; are idempotent: deleting user 42 five times leaves the database in the same state as deleting it once. &lt;code&gt;POST&lt;/code&gt; is not: five &lt;code&gt;POST /users&lt;/code&gt; requests create five different users.&lt;/p&gt;

&lt;p&gt;This matters for reliability. Networks fail. Clients timeout. If a &lt;code&gt;POST&lt;/code&gt; request times out, the client doesn't know whether the server processed it — retrying might create a duplicate. The fix is an &lt;strong&gt;idempotency key&lt;/strong&gt; : a unique client-generated token (a UUID works well) sent in a header like &lt;code&gt;Idempotency-Key: &amp;lt;uuid&amp;gt;&lt;/code&gt;. The server stores the key with the result and returns the cached result for duplicate keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL design guidelines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use nouns, not verbs: &lt;code&gt;/users&lt;/code&gt; not &lt;code&gt;/getUsers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use plurals for collections: &lt;code&gt;/orders&lt;/code&gt; not &lt;code&gt;/order&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Nest for relationships: &lt;code&gt;/users/42/orders&lt;/code&gt; for user 42's orders&lt;/li&gt;
&lt;li&gt;Use query parameters for filtering: &lt;code&gt;/orders?status=active&amp;amp;limit=20&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keep URLs lowercase with hyphens: &lt;code&gt;/order-items&lt;/code&gt; not &lt;code&gt;/orderItems&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  HTTP Status Codes: The Language Clients Speak
&lt;/h2&gt;

&lt;p&gt;HTTP status codes are how your API communicates what happened. Using them correctly means clients can respond appropriately without parsing response bodies. The categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;The request succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3xx&lt;/td&gt;
&lt;td&gt;Redirection&lt;/td&gt;
&lt;td&gt;The resource moved (rare in APIs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;Client error&lt;/td&gt;
&lt;td&gt;The request was malformed or unauthorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;Server error&lt;/td&gt;
&lt;td&gt;Something went wrong on your end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The codes you'll use most often:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200 OK&lt;/strong&gt; — standard success for &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;201 Created&lt;/strong&gt; — &lt;code&gt;POST&lt;/code&gt; that created a new resource (include a &lt;code&gt;Location&lt;/code&gt; header pointing to the new resource)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;204 No Content&lt;/strong&gt; — success with no response body (common for &lt;code&gt;DELETE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400 Bad Request&lt;/strong&gt; — the request was malformed (missing required field, invalid type)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401 Unauthorized&lt;/strong&gt; — authentication failed (missing or invalid credentials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;403 Forbidden&lt;/strong&gt; — authenticated but not allowed to access this resource&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 Not Found&lt;/strong&gt; — the resource doesn't exist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;409 Conflict&lt;/strong&gt; — the request conflicts with the current state (duplicate email, version mismatch)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;422 Unprocessable Entity&lt;/strong&gt; — the request was well-formed but semantically invalid (validation errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 Too Many Requests&lt;/strong&gt; — rate limit exceeded (include &lt;code&gt;Retry-After&lt;/code&gt; header)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500 Internal Server Error&lt;/strong&gt; — something went wrong on your end (don't expose details)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The 401 vs 403 distinction trips up many developers.&lt;/strong&gt; 401 means "I don't know who you are" (authentication). 403 means "I know who you are, but you can't do this" (authorization). A user without a valid token gets 401. A regular user trying to access an admin endpoint gets 403.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;429 Too Many Requests&lt;/strong&gt; deserves its own section — it's the rate limit response, and how you implement it determines whether your API survives a traffic spike or a misbehaving client.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rate Limiting: Protecting Your Infrastructure
&lt;/h2&gt;

&lt;p&gt;Rate limiting is the practice of controlling how many requests a client can make in a given time window. It protects your API from abuse, prevents a single tenant from monopolizing resources, and ensures fair access across all users.&lt;/p&gt;

&lt;p&gt;The challenge is doing this without frustrating legitimate users. A developer integrating with your API will hit rate limits during testing. A batch job running at midnight will hit rate limits. A user with a flaky network that retries failed requests will hit rate limits. Good rate limiting distinguishes between a client that's slightly over its limit and one that's attacking your API.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three Algorithms
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fixed window&lt;/strong&gt; is the simplest approach: allow N requests per window (e.g., 1,000 per hour), reset the counter at the start of each window. The implementation is trivial — a counter per client that resets on a timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;]&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;if&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The weakness is the &lt;strong&gt;edge effect&lt;/strong&gt; : a client that sends 1,000 requests at 10:59 and another 1,000 at 11:01 has sent 2,000 requests in 2 minutes, despite the "1,000 per hour" limit. The fixed window doesn't see that the two bursts overlap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window log&lt;/strong&gt; stores every request timestamp for every client and counts how many fall within the current window. It's precise but expensive: storing millions of timestamps per client is memory-intensive. A common optimization is the &lt;strong&gt;sliding window counter&lt;/strong&gt; , which combines the current window's count with the previous window's count, weighted by how far into the current window you are:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;co&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;c&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;rre&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="mord mathnormal"&gt;in&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ze&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;im&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;in&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;w&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="mord mathnormal"&gt;re&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;This approximates the sliding window without storing every timestamp, at the cost of allowing slightly more than the limit at window boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token bucket&lt;/strong&gt; is the most flexible algorithm and the one used by most production APIs (including GitHub and Stripe). Imagine a bucket that holds a maximum of &lt;code&gt;B&lt;/code&gt; tokens. Tokens are added at a constant rate &lt;code&gt;r&lt;/code&gt; per second (e.g., 10 tokens per second). Each request consumes one token. If the bucket is empty, the request is rejected with 429.&lt;/p&gt;

&lt;p&gt;The token bucket handles bursts naturally: a client that's been idle for a minute has accumulated 60 tokens and can burst to 60 requests immediately. But sustained traffic cannot exceed the refill rate. The algorithm is:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;min&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;B&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;If &lt;code&gt;tokens &amp;gt;= 0&lt;/code&gt;, the request is allowed. If &lt;code&gt;tokens &amp;lt; 0&lt;/code&gt;, it's rejected.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/api-rate-limit-cost-calculator" rel="noopener noreferrer"&gt;API Rate Limit &amp;amp; Cost Calculator&lt;/a&gt; computes utilization, throttled requests, and monthly cost for any of these algorithms given your limit, load, and price per request. Use it to estimate whether your current limits can handle a 10× traffic spike, or to plan the cost of a rate-limited batch job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Communicating Limits to Clients
&lt;/h3&gt;

&lt;p&gt;A rate limit without communication is just an error. Good APIs tell clients three things in every response:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What the limit is&lt;/strong&gt; : &lt;code&gt;X-RateLimit-Limit: 1000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many requests remain&lt;/strong&gt; : &lt;code&gt;X-RateLimit-Remaining: 847&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the limit resets&lt;/strong&gt; : &lt;code&gt;X-RateLimit-Reset: 1687305600&lt;/code&gt; (Unix timestamp)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the limit is exceeded, return &lt;strong&gt;429 Too Many Requests&lt;/strong&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header that tells the client how long to wait before retrying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;429&lt;/span&gt; &lt;span class="ne"&gt;Too Many Requests&lt;/span&gt;
&lt;span class="na"&gt;Retry-After&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60&lt;/span&gt;
&lt;span class="na"&gt;X-RateLimit-Limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1000&lt;/span&gt;
&lt;span class="na"&gt;X-RateLimit-Remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;X-RateLimit-Reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1687305600&lt;/span&gt;

{
  "error": "rate_limit_exceeded",
  "message": "You have exceeded the rate limit of 1000 requests per hour.",
  "retry_after": 60
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Retry-After&lt;/code&gt; header is in seconds (or an HTTP date). A well-behaved client reads this header and waits before retrying — instead of retrying immediately and making the problem worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical implementation:&lt;/strong&gt; Most production APIs implement rate limiting at the API gateway or load balancer level (Kong, NGINX, AWS API Gateway, Cloudflare) rather than in application code. This offloads the work and applies limits before requests reach your application servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authentication: Knowing Who's Calling
&lt;/h2&gt;

&lt;p&gt;Every API needs to answer two questions: &lt;em&gt;who is this?&lt;/em&gt; (authentication) and &lt;em&gt;are they allowed to do this?&lt;/em&gt; (authorization). The most common approaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  API Keys
&lt;/h3&gt;

&lt;p&gt;The simplest approach: give each client a unique key, and require it in every request (usually via header: &lt;code&gt;Authorization: Bearer &amp;lt;key&amp;gt;&lt;/code&gt; or &lt;code&gt;X-API-Key: &amp;lt;key&amp;gt;&lt;/code&gt;). The server looks up the key to identify the client and check permissions.&lt;/p&gt;

&lt;p&gt;API keys are easy to implement but have a weakness: they don't expire. If a key is leaked (committed to a GitHub repo, logged in an error message, intercepted over an unencrypted connection), anyone who has it can impersonate that client. Mitigations: require HTTPS, allow clients to rotate keys, and set expiration dates on keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  JWT (JSON Web Tokens)
&lt;/h3&gt;

&lt;p&gt;A JWT is a self-contained token that encodes claims (user ID, permissions, expiration) in a JSON payload, signed by the server. The client sends the token in the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header. The server verifies the signature and extracts the claims — no database lookup required.&lt;/p&gt;

&lt;p&gt;A JWT has three parts, each Base64URL-encoded and separated by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNjg3MzA1NjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt; (&lt;code&gt;eyJhbGciOiJIUzI1NiJ9&lt;/code&gt;): algorithm and token type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt; (&lt;code&gt;eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNjg3MzA1NjAwfQ&lt;/code&gt;): claims — user_id, expiration, roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt; (&lt;code&gt;SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&lt;/code&gt;): HMAC of header+payload with server secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical claim is &lt;code&gt;exp&lt;/code&gt; (expiration). A JWT without an expiration is valid forever — if leaked, it's a permanent access token. Set short expirations (15 minutes to 1 hour) and use refresh tokens for longer sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT ≠ encryption.&lt;/strong&gt; The payload is signed, not encrypted — anyone who intercepts the token can read the claims (that's why you should never put secrets in a JWT). The signature only guarantees that the token was issued by someone with the server secret and hasn't been tampered with. The &lt;a href="https://notacalculator.com/calculator/jwt-decoder" rel="noopener noreferrer"&gt;JWT Decoder&lt;/a&gt; parses tokens client-side, showing the header, payload, and signature — useful for debugging authentication flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  OAuth 2.0
&lt;/h3&gt;

&lt;p&gt;OAuth 2.0 is a delegation protocol: it lets a user grant a third-party application limited access to their resources without sharing their password. The flow: the user clicks "Log in with Google," Google asks for permission, and issues an access token that the third-party app can use on the user's behalf.&lt;/p&gt;

&lt;p&gt;For machine-to-machine APIs (where there's no user to click "Log in"), use the &lt;strong&gt;client credentials&lt;/strong&gt; flow: the client exchanges its client ID and secret for an access token. This is what backend services use to communicate with each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security: The Mistakes That Get You Hacked
&lt;/h2&gt;

&lt;p&gt;APIs are the most common attack surface for web applications. The OWASP API Security Top 10 lists the most critical risks. The ones every developer should know:&lt;/p&gt;

&lt;h3&gt;
  
  
  Broken Object-Level Authorization (BOLA)
&lt;/h3&gt;

&lt;p&gt;The most common API vulnerability. If &lt;code&gt;GET /users/42&lt;/code&gt; returns user 42's data, but the server doesn't check that the authenticated user is user 42 (or an admin), then any authenticated user can access any other user's data by changing the ID in the URL. Every endpoint that takes an ID must check authorization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Excessive Data Exposure
&lt;/h3&gt;

&lt;p&gt;APIs often return more data than the client needs. If &lt;code&gt;GET /users/42&lt;/code&gt; returns the user's email, phone, address, and hashed password, the client may only display the name — but the sensitive data is now in the browser's network tab, in logs, and in any proxy between client and server. Return only the fields the client needs, and use different response schemas for different roles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lack of Resources &amp;amp; Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Without rate limits, an attacker can brute-force passwords, scrape data, or overwhelm your API with requests. This is the failure mode that caused the $10 million outage described earlier. Rate limit authentication endpoints aggressively (5 attempts per minute per IP) and API endpoints per-client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mass Assignment
&lt;/h3&gt;

&lt;p&gt;If your API accepts a JSON body and binds it directly to a database model without filtering, an attacker can set fields they shouldn't control. A &lt;code&gt;PATCH /users/42&lt;/code&gt; endpoint that accepts &lt;code&gt;{ "role": "admin" }&lt;/code&gt; because the request body is bound directly to the model is a mass assignment vulnerability. Whitelist the fields that can be updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Headers
&lt;/h3&gt;

&lt;p&gt;Every API response should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Strict-Transport-Security: max-age=31536000&lt;/code&gt; — force HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; — prevent MIME sniffing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cache-Control: no-store&lt;/code&gt; — don't cache responses with sensitive data&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Tips for API Development
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always version your API.&lt;/strong&gt; Use URL versioning (&lt;code&gt;/v1/users&lt;/code&gt;) or header versioning (&lt;code&gt;Accept: application/vnd.api.v1+json&lt;/code&gt;). Never release a breaking change without a version bump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use pagination for collections.&lt;/strong&gt; Returning 100,000 records in one response is a memory bomb. Use cursor-based pagination (&lt;code&gt;?cursor=&amp;lt;token&amp;gt;&amp;amp;limit=20&lt;/code&gt;) for large datasets, or offset-based (&lt;code&gt;?offset=0&amp;amp;limit=20&lt;/code&gt;) for smaller ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return consistent error formats.&lt;/strong&gt; Every error response should have the same structure: &lt;code&gt;{ "error": { "code": "...", "message": "...", "details": [...] } }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log requests, not bodies.&lt;/strong&gt; Log the method, URL, status code, and response time. Never log request bodies — they may contain passwords or tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use HTTPS everywhere.&lt;/strong&gt; No exceptions. Even internal APIs should use TLS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set request size limits.&lt;/strong&gt; A &lt;code&gt;POST&lt;/code&gt; endpoint without a body size limit is vulnerable to memory exhaustion attacks. Limit to what you actually need (1 MB for most APIs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement circuit breakers.&lt;/strong&gt; If a dependent service is failing, stop calling it after N consecutive failures and return a fallback response. This prevents cascading failures.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between REST and GraphQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;REST models your API as resources identified by URLs, with HTTP methods as verbs. GraphQL uses a single endpoint and lets clients specify exactly what data they need in the query. REST is simpler and cacheable; GraphQL is more flexible for complex data requirements.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is idempotency and why does it matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Idempotency means making the same request multiple times produces the same result as making it once. GET, PUT, and DELETE are idempotent; POST is not. For non-idempotent operations, use an idempotency key (a UUID) so retries don't create duplicates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between 401 and 403?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;401 Unauthorized means authentication failed (missing or invalid credentials) — I don't know who you are. 403 Forbidden means you're authenticated but not allowed to access this resource — I know who you are, but you can't do this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is a JWT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A JSON Web Token is a self-contained token that encodes claims (user ID, permissions, expiration) in a JSON payload, signed by the server. The server verifies the signature and extracts the claims without a database lookup. JWTs are signed, not encrypted — never put secrets in them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the best rate limiting algorithm?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Token bucket is the most common in production (used by GitHub and Stripe) because it handles bursts naturally while capping sustained traffic. Fixed window is simpler but allows 2× bursts at window boundaries. Sliding window is more precise but more expensive to implement.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What headers should a rate-limited API return?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;X-RateLimit-Limit (the limit), X-RateLimit-Remaining (requests left), and X-RateLimit-Reset (when the limit resets). On 429 responses, include Retry-After (seconds to wait before retrying).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I protect against BOLA?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Broken Object-Level Authorization — the most common API vulnerability. Every endpoint that takes an ID must check that the authenticated user is authorized to access that specific resource. Don't rely on clients to only request their own data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should API keys expire?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Yes. Keys that never expire are a permanent liability if leaked. Set expiration dates (90 days is common) and allow clients to rotate keys. Require HTTPS to prevent interception.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/api-design-rate-limiting-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>guide</category>
      <category>api</category>
      <category>restapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Web Performance on a Budget: Bundle Size, API Limits, and Database Indexes</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Tue, 25 Aug 2026 05:09:22 +0000</pubDate>
      <link>https://dev.to/apeder/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes-44c6</link>
      <guid>https://dev.to/apeder/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes-44c6</guid>
      <description>&lt;p&gt;&lt;em&gt;The three budgets every web app hits: bundle size, API rate limits, and database index selectivity. Learn the formulas, thresholds, and PR checklist.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every web app has three budgets, and only one of them lives in your code. The bundle budget lives in the browser — 244 kB gzip is where Lighthouse starts penalizing Time to Interactive. The API budget lives at the provider — 5,000 requests per hour on GitHub, 500 per minute on OpenAI Tier 1, 100 writes per second on Stripe. The database budget lives in the planner — an index with 0.5 selectivity will be ignored and the query will seq scan 50,000 rows. Exceed any of the three and the user pays: slower paint, &lt;code&gt;429 Too Many Requests&lt;/code&gt;, or a query that times out.&lt;/p&gt;

&lt;p&gt;The budgets are linked by the same habit that breaks them: averaging. A bundle that is 71 kB &lt;code&gt;lodash&lt;/code&gt; full on average is 7.2 kB &lt;code&gt;lodash/get&lt;/code&gt; when imported correctly — the average hides the choice. An API that is 20 requests per minute on average is 800 per minute during a burst deploy — the average hides the spike. A table where &lt;code&gt;status&lt;/code&gt; has two values has selectivity 0.5 on average but 0.95 for &lt;code&gt;active&lt;/code&gt; and 0.05 for &lt;code&gt;pending&lt;/code&gt; — the average hides the skew.&lt;/p&gt;

&lt;p&gt;This guide makes the three budgets explicit, with the formulas, the thresholds, and a single PR checklist that covers frontend, backend, and database. Every number is reproducible with the three calculators that accompany it — the &lt;a href="https://notacalculator.com/calculator/bundle-size-impact-calculator" rel="noopener noreferrer"&gt;Bundle Size Impact Calculator&lt;/a&gt;, the &lt;a href="https://notacalculator.com/calculator/api-rate-limit-cost-calculator" rel="noopener noreferrer"&gt;API Rate Limit &amp;amp; Cost Calculator&lt;/a&gt;, and the &lt;a href="https://notacalculator.com/calculator/sql-index-selectivity-calculator" rel="noopener noreferrer"&gt;SQL Index Selectivity Calculator&lt;/a&gt;. For the network that connects them, the &lt;a href="https://notacalculator.com/calculator/bandwidth-calculator" rel="noopener noreferrer"&gt;Bandwidth Calculator&lt;/a&gt; sizes the pipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bundle Size — The Frontend Budget
&lt;/h2&gt;

&lt;p&gt;The smallest budget is the one the user downloads. A parsed package of 71.0 kB &lt;code&gt;lodash&lt;/code&gt; full is 24.4 kB gzip and 20.1 kB brotli. On a 1.6 Mbps 3G link that is 122 ms of transfer before &lt;code&gt;first-paint&lt;/code&gt;; on 9 Mbps 4G it is 22 ms. The same utility as &lt;code&gt;lodash/get&lt;/code&gt; is 7.2 kB parsed, 2.9 kB gzip, 15 ms on 3G — an 8× saving from a one-line import change.&lt;/p&gt;

&lt;p&gt;The formula is linear:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;S&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;gzip&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;S&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;parsed&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;gzip&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;B&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1000&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;S&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;gzip&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;RTT&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where B is Mbps and RTT is round-trip in seconds. The budget share is &lt;code&gt;S_gzip / 244 × 100&lt;/code&gt;. The &lt;a href="https://notacalculator.com/calculator/bundle-size-impact-calculator" rel="noopener noreferrer"&gt;Bundle Size Impact Calculator&lt;/a&gt; evaluates all three plus brotli.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code — the 8× mistake and the fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import _ from 'lodash'; // 71.0 kB parsed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import get from 'lodash/get'; // 7.2 kB parsed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import { get } from 'lodash-es'; // tree-shaken, 7.2 kB&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bundlers like webpack only tree-shake &lt;code&gt;lodash-es&lt;/code&gt; and named ESM imports — the default &lt;code&gt;lodash&lt;/code&gt; import is not shaken.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Reference — popular packages (minified+gzipped):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Parsed (kB)&lt;/th&gt;
&lt;th&gt;Gzip (kB)&lt;/th&gt;
&lt;th&gt;% of 244 kB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lodash (full)&lt;/td&gt;
&lt;td&gt;71.0&lt;/td&gt;
&lt;td&gt;24.4&lt;/td&gt;
&lt;td&gt;10.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lodash/get&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;2.9&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;moment&lt;/td&gt;
&lt;td&gt;66.5&lt;/td&gt;
&lt;td&gt;19.8&lt;/td&gt;
&lt;td&gt;8.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date-fns&lt;/td&gt;
&lt;td&gt;19.0&lt;/td&gt;
&lt;td&gt;5.8&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;react-dom&lt;/td&gt;
&lt;td&gt;130.0&lt;/td&gt;
&lt;td&gt;42.0&lt;/td&gt;
&lt;td&gt;17.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;em&gt;Gzip sizes — single import choice moves 21.5 kB (107 ms on 3G). Replace moment with date-fns and save another 14 kB (70 ms).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; set a CI budget of 244 kB gzip total, 130 kB per route, and paste the calculator's &lt;code&gt;Budget%&lt;/code&gt; into the PR description — future reviewers see the cost without rebuilding.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. API Rate Limits — The Backend Budget
&lt;/h2&gt;

&lt;p&gt;If the bundle budget is about bytes, the API budget is about tokens per minute. GitHub's 5,000 per hour is 83.3 per minute; OpenAI's Tier 1 is 500 per minute; Stripe's writes are 100 per second. All are token buckets: refill at a fixed rate up to a burst capacity. You can burst to capacity instantly, but sustained throughput cannot exceed refill.&lt;/p&gt;

&lt;p&gt;Let L = limit per minute, D = demand per minute, r = retry fraction, p = price per request:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;U&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;D&lt;/span&gt;&lt;span class="mord"&gt;/&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop"&gt;max&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;D&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;T&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;r&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Monthly cost (30-day month, 43,200 minutes):&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;month&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;D&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;43200&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;At $2 per 1K ($0.002 per request), 500 per minute limit with 800 per minute burst and 30% retry gives U = 160%, T = 300 throttled/min, R = 90 extra/min, and Deff = 890/min during the burst — $3.56 for two minutes, or $76k per month if sustained. The fix is not more retries but a queue that smooths the burst.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code — queue instead of retry:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import pLimit from 'p-limit'; const limit = pLimit(10); // 10/min&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await Promise.all(urls.map(url &amp;gt; limit(() =&amp;gt; fetch(url))));&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Batch where the API allows it — GitHub GraphQL and Stripe batches turn N requests into 1.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/api-rate-limit-cost-calculator" rel="noopener noreferrer"&gt;API Rate Limit &amp;amp; Cost Calculator&lt;/a&gt; evaluates &lt;code&gt;U&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt;, &lt;code&gt;R&lt;/code&gt;, and &lt;code&gt;C_month&lt;/code&gt; for any per-second/minute/hour limit.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. SQL Index Selectivity — The Database Budget
&lt;/h2&gt;

&lt;p&gt;The database budget is the most misread. Selectivity &lt;code&gt;S = R / N&lt;/code&gt; (rows returned / total rows) decides if the planner uses the index. At &lt;code&gt;S = 0.5&lt;/code&gt; (50% of rows) the index is ignored; at &lt;code&gt;S = 0.001%&lt;/code&gt; (1 row) it is always used. The threshold is around 5–20% with default costs.&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;S&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;span class="mord"&gt;/&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;seq&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1.0&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;idx&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;4.0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;If &lt;code&gt;C_idx &amp;lt; C_seq&lt;/code&gt; the planner chooses index scan. For &lt;code&gt;status = 'active'&lt;/code&gt; on 100k rows where 95k are active, &lt;code&gt;S = 0.95&lt;/code&gt;, &lt;code&gt;C_idx = 380k&lt;/code&gt; vs &lt;code&gt;C_seq = 100k&lt;/code&gt; — seq scan wins. For &lt;code&gt;email = 'a@b.com'&lt;/code&gt; with one row, &lt;code&gt;S = 0.00001&lt;/code&gt;, &lt;code&gt;C_idx = 4&lt;/code&gt; vs &lt;code&gt;100k&lt;/code&gt; — index wins 25,000×.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code — check before indexing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SELECT COUNT(DISTINCT status) FROM users; -- C&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXPLAIN SELECT * FROM users WHERE status='active'; -- rows, cost&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;C &amp;lt; 100&lt;/code&gt; on 100k rows, a single-column index on that column will be ignored for most values — use a composite &lt;code&gt;(status, created_at)&lt;/code&gt; instead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distinct (C)&lt;/th&gt;
&lt;th&gt;Rows/value&lt;/th&gt;
&lt;th&gt;Selectivity&lt;/th&gt;
&lt;th&gt;Planner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100,000 (unique)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.001%&lt;/td&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.10%&lt;/td&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Borderline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;Seq Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;em&gt;Selectivity % (log) — unique is 0.001% (always indexed), 2 distinct 50% (never alone).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/sql-index-selectivity-calculator" rel="noopener noreferrer"&gt;SQL Index Selectivity Calculator&lt;/a&gt; reports &lt;code&gt;S&lt;/code&gt;, efficiency &lt;code&gt;1−S&lt;/code&gt;, and the verdict with cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Putting It Together — The PR Checklist
&lt;/h2&gt;

&lt;p&gt;A single PR that adds a dependency, a new API call, and a migration can bust all three budgets at once. Check them together:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Budget&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bundle&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;S_gzip / 244 &amp;lt; 5%&lt;/code&gt; per new dep&lt;/td&gt;
&lt;td&gt;&lt;a href="https://notacalculator.com/calculator/bundle-size-impact-calculator" rel="noopener noreferrer"&gt;Bundle Size Impact&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;CI 244 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;U = D/L &amp;lt; 80%&lt;/code&gt; sustained&lt;/td&gt;
&lt;td&gt;&lt;a href="https://notacalculator.com/calculator/api-rate-limit-cost-calculator" rel="noopener noreferrer"&gt;API Rate Limit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Alert at 80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;S = R/N &amp;lt; 10%&lt;/code&gt; for indexed filter&lt;/td&gt;
&lt;td&gt;&lt;a href="https://notacalculator.com/calculator/sql-index-selectivity-calculator" rel="noopener noreferrer"&gt;SQL Index Selectivity&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;EXPLAIN rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Begginer:&lt;/em&gt; paste the three calculator outputs into the PR description — reviewers see the numbers without pulling the branch. &lt;em&gt;Senior:&lt;/em&gt; add the three gates to CI — &lt;code&gt;bundle&lt;/code&gt; via Lighthouse CI, &lt;code&gt;API&lt;/code&gt; via &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; header check, &lt;code&gt;index&lt;/code&gt; via &lt;code&gt;EXPLAIN&lt;/code&gt; in migration tests. &lt;em&gt;Sensei:&lt;/em&gt; make the checklist a required GitHub PR template so every service that ships JS, calls an API, and migrates a table is measured the same way.&lt;/p&gt;

&lt;p&gt;For the network that connects them, the &lt;a href="https://notacalculator.com/calculator/bandwidth-calculator" rel="noopener noreferrer"&gt;Bandwidth Calculator&lt;/a&gt; converts &lt;code&gt;S_gzip&lt;/code&gt; to wall-clock time on 3G/4G.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Case Study — One PR, Three Budgets
&lt;/h2&gt;

&lt;p&gt;A real pull request that adds a dashboard with a chart, an analytics API call, and a new filter illustrates how the budgets interact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PR:&lt;/strong&gt; Add &lt;code&gt;recharts&lt;/code&gt; for a new &lt;code&gt;RevenueChart&lt;/code&gt; component, fetch &lt;code&gt;/api/revenue?range=30d&lt;/code&gt; on page load, and add &lt;code&gt;WHERE status = 'active' AND region = 'EU'&lt;/code&gt; to the revenue query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundle check:&lt;/strong&gt; &lt;code&gt;recharts&lt;/code&gt; is 130 kB parsed, 42 kB gzip — 17.2% of the 244 kB budget. The existing bundle is 180 kB gzip, so the new total is 222 kB (91%). The &lt;a href="https://notacalculator.com/calculator/bundle-size-impact-calculator" rel="noopener noreferrer"&gt;Bundle Size Impact Calculator&lt;/a&gt; shows 42 kB + 3G 210 ms. The fix is code-splitting: move &lt;code&gt;RevenueChart&lt;/code&gt; to &lt;code&gt;React.lazy&lt;/code&gt; so the initial route stays at 180 kB and the chart chunk loads on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API check:&lt;/strong&gt; The dashboard polls every 30 seconds, so 2 requests per minute per user. With 500 concurrent users, D = 1,000 per minute. The API limit is 500 per minute, so U = 200%, T = 500 throttled/min. With 30% retry, R = 150, Deff = 1,150. The &lt;a href="https://notacalculator.com/calculator/api-rate-limit-cost-calculator" rel="noopener noreferrer"&gt;API Rate Limit &amp;amp; Cost Calculator&lt;/a&gt; shows the cost and the need for a queue. The fix is caching the &lt;code&gt;GET&lt;/code&gt; for 60 seconds and batching the poll to 1 per minute via &lt;code&gt;SWR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database check:&lt;/strong&gt; The new filter &lt;code&gt;WHERE status='active'&lt;/code&gt; on 500k rows where 80% are active has S = 0.80, efficiency 20% — the planner will seq scan. Adding &lt;code&gt;AND region='EU'&lt;/code&gt; where EU is 10% of rows makes the composite &lt;code&gt;S = 0.80 × 0.10 = 0.08&lt;/code&gt; (8%) — now selective enough for a composite index &lt;code&gt;(region, status)&lt;/code&gt; or &lt;code&gt;(status, region)&lt;/code&gt; depending on which column is more selective. The &lt;a href="https://notacalculator.com/calculator/sql-index-selectivity-calculator" rel="noopener noreferrer"&gt;SQL Index Selectivity Calculator&lt;/a&gt; confirms S = 8% → Index Scan, cost 32k vs seq 500k.&lt;/p&gt;

&lt;p&gt;The PR ships when all three checks are green: bundle under 244 kB initial, API U &amp;lt; 80%, and &lt;code&gt;EXPLAIN&lt;/code&gt; shows Index Scan. One budget in the red is a revert.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Monitoring After Ship
&lt;/h2&gt;

&lt;p&gt;Budgets are not one-time checks; they drift. Bundle size grows with every &lt;code&gt;npm install&lt;/code&gt;, API load grows with every new replica, and selectivity shifts as the table fills. Track the three on the same dashboard: Lighthouse CI for bundle (&lt;code&gt;S_gzip&lt;/code&gt; trend), &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; header sampling for API (&lt;code&gt;U&lt;/code&gt; over time), and &lt;code&gt;pg_stat_user_indexes&lt;/code&gt; scans for database (&lt;code&gt;S&lt;/code&gt; per index). When any of the three crosses 80% of its budget, the next feature PR should include a budget-reducing change — a lazy chunk, a cache, or a composite index — before new functionality.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Practical Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure with the same tool you ship with.&lt;/strong&gt; &lt;code&gt;esbuild --metafile&lt;/code&gt; vs Bundlephobia differ by minifier — pick one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import the file, not the barrel.&lt;/strong&gt; &lt;code&gt;lodash/get&lt;/code&gt; vs &lt;code&gt;lodash&lt;/code&gt; is 8× — the same holds for &lt;code&gt;date-fns/format&lt;/code&gt; vs &lt;code&gt;date-fns&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue bursts, don't retry bursts.&lt;/strong&gt; A &lt;code&gt;p-limit&lt;/code&gt; queue at &lt;code&gt;L&lt;/code&gt; eliminates throttling without retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check cardinality before indexing.&lt;/strong&gt; &lt;code&gt;COUNT(DISTINCT col)&lt;/code&gt; &amp;lt; 100 on 100k rows → composite, not single-column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;EXPLAIN&lt;/code&gt; rows, not just the index list.&lt;/strong&gt; &lt;code&gt;rows = S×N&lt;/code&gt; tells you the selectivity the planner actually used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version your budgets today.&lt;/strong&gt; Keep the three thresholds (244 kB, 80% API, 10% selectivity) in a &lt;code&gt;budgets.json&lt;/code&gt; or &lt;code&gt;lighthouse-budget.json&lt;/code&gt; so CI and humans share the same numbers — drift happens when the budget lives only in a comment.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the 244 kB bundle budget?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;A common Lighthouse threshold where total gzip over ~244 kB starts penalizing Time to Interactive. Use it as a reference; your product's budget may be 170 or 500 kB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Gzip or brotli for budgeting?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Gzip for the conservative gate (all CDNs support it), brotli for the optimistic check (most modern CDNs serve brotli, ~15 percent smaller).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When should I retry a 429?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Retry 20–30 percent once with exponential backoff starting from the Retry-After header. Retrying 100 percent on an overloaded bucket just re-throttles.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: When does an index get ignored?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;When selectivity is high — roughly above 10 percent with default costs. On 100k rows, keeping 10k rows (10 percent) is borderline; keeping 50k (50 percent) is always a seq scan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I fix low selectivity with an index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Alone, no — a single-column index on a 2-value column is 50 percent selective. As part of a composite (status, created_at) that raises cardinality, yes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I know my API's limit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Check the docs (GitHub 5,000/hour, OpenAI 500/minute, Stripe 100 writes/second) and the response headers X-RateLimit-Limit/Remaining/Reset. The calculator handles per second/minute/hour.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need all three calculators for every PR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;No — check the budget you touch. New dep → bundle, new API call → rate limit, new WHERE → selectivity. If a PR touches all three, check all three.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/web-performance-budget-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>guide</category>
      <category>webdev</category>
      <category>api</category>
      <category>sql</category>
    </item>
    <item>
      <title>Design Tools Guide - Contrast, Golden Ratio and Aspect Ratio</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:24:27 +0000</pubDate>
      <link>https://dev.to/apeder/design-tools-guide-contrast-golden-ratio-and-aspect-ratio-1kmi</link>
      <guid>https://dev.to/apeder/design-tools-guide-contrast-golden-ratio-and-aspect-ratio-1kmi</guid>
      <description>&lt;p&gt;&lt;em&gt;WCAG color contrast, the golden ratio, and aspect ratios — how to combine readability and proportion tools for better layouts.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Design decisions that look subjective usually reduce to two measurable questions: can people read it, and does the layout feel balanced? The first is answered by color contrast — the numerical relationship between text and background that determines legibility for everyone, and especially for people with low vision. The second is answered by proportion — how the dimensions of a frame, a block of type, or a page relate to one another. Both questions have precise answers, and both are what make the difference between a component that merely exists and one that communicates.&lt;/p&gt;

&lt;p&gt;This guide brings the two questions together because working designers solve them together. You do not choose a contrast ratio and a frame size in isolation: a button's color must pass WCAG against the panel behind it while the panel itself sits at a proportion that fits the canvas. The &lt;a href="https://notacalculator.com/calculator/contrast-checker-calculator" rel="noopener noreferrer"&gt;Contrast Checker&lt;/a&gt; measures readability, the &lt;a href="https://notacalculator.com/calculator/golden-ratio-calculator" rel="noopener noreferrer"&gt;Golden Ratio Calculator&lt;/a&gt; measures balance, the &lt;a href="https://notacalculator.com/calculator/aspect-ratio-calculator" rel="noopener noreferrer"&gt;Aspect Ratio Calculator&lt;/a&gt; measures shape, and the &lt;a href="https://notacalculator.com/calculator/color-converter" rel="noopener noreferrer"&gt;Color Converter&lt;/a&gt; and &lt;a href="https://notacalculator.com/calculator/ratio-calculator" rel="noopener noreferrer"&gt;Ratio Calculator&lt;/a&gt; fill in the conversion work those three rely on.&lt;/p&gt;

&lt;p&gt;By the end you will know what each tool computes, when it matters, where the numbers come from, and — just as important — where each number stops being useful. The goal is not to replace design judgment with formulas, but to give judgment a numerical backbone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Design Decisions: Readability and Proportion
&lt;/h2&gt;

&lt;p&gt;Every visual decision can be filed under one of two headings: contrast or proportion. Readability is a contrast problem. Typography, color, and luminance all converge on one question — is there enough difference between the text and what is behind it for the eye to separate them? Proportion is a shape problem. It asks how the parts of a composition relate: how wide a block of type should be relative to its height, how a poster's panels divide, how an image frames a subject.&lt;/p&gt;

&lt;p&gt;Contrast is enforced by a standard. The Web Content Accessibility Guidelines (WCAG) set numeric thresholds, tested by browsers, tools, and increasingly by legal requirement, so a designer can compute pass or fail with certainty. Proportion is guided, not enforced. There is no WCAG for a golden rectangle, because balance is cultural and contextual — but there are geometric relationships, like the golden ratio, that recur across centuries of design because they are consistently pleasant to look at.&lt;/p&gt;

&lt;p&gt;The distinction matters because it changes how you use the numbers. A contrast ratio below the WCAG threshold is objectively wrong for body text — no amount of aesthetic judgment justifies it. A proportion that ignores the golden ratio is not wrong, merely one choice among many. Designers who treat every numeric tool as a hard rule end up with rigid work; designers who treat every tool as optional end up with unreadable work. The skill is knowing which number is a gate and which is a guide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Color Contrast and WCAG Accessibility
&lt;/h2&gt;

&lt;p&gt;Color contrast is the luminance difference between two colors, computed by comparing their relative luminances and expressed as a ratio from 1:1 (identical colors) to 21:1 (black on white). The ratio is standardized by WCAG and is the same ratio every contrast checker on the web reports, so a value computed here matches the value your testing tool reports.&lt;/p&gt;

&lt;p&gt;The relative luminance of a color is a weighted sum of its red, green, and blue channels after each channel is linearized:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.2126&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.7152&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;G&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.0722&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;B&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;′&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where each channel value is converted from the sRGB 8-bit range (0–255) using the piecewise curve defined in the WCAG specification. Two colors are compared by dividing the lighter luminance plus 0.05 by the darker luminance plus 0.05:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;contrast&amp;nbsp;ratio&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;darker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.05&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;lighter&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.05&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;WCAG sets the following pass thresholds for normal text, large text, and user interface components:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Minimum ratio&lt;/th&gt;
&lt;th&gt;Applies to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AA normal text&lt;/td&gt;
&lt;td&gt;4.5:1&lt;/td&gt;
&lt;td&gt;All body copy, buttons, form labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AA large text&lt;/td&gt;
&lt;td&gt;3:1&lt;/td&gt;
&lt;td&gt;Text at least 24px, or 19px bold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAA normal text&lt;/td&gt;
&lt;td&gt;7:1&lt;/td&gt;
&lt;td&gt;Extended reading, long-form content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAA large text&lt;/td&gt;
&lt;td&gt;4.5:1&lt;/td&gt;
&lt;td&gt;Large text under the strictest standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI components&lt;/td&gt;
&lt;td&gt;3:1&lt;/td&gt;
&lt;td&gt;Focus indicators, icons, input borders&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The thresholds are not arbitrary. Four and a half to one is calibrated so that people with typical low-vision contrast sensitivity — roughly the level of someone in their eighties or with moderate visual impairment — can still read body text. AAA pushes that further to cover people with more severe loss. Horton and Quesenbery make the accessibility argument concrete in &lt;em&gt;A Web for Everyone&lt;/em&gt;: contrast problems are the single most common reason users with low vision abandon a site, and fixing them costs nothing but a choice of color.&lt;/p&gt;

&lt;p&gt;Practical steps when a color pair fails: darken the text, lighten the background, or widen the type. The &lt;a href="https://notacalculator.com/calculator/color-converter" rel="noopener noreferrer"&gt;Color Converter&lt;/a&gt; helps here — converting the failing color to HSL lets you drop the lightness channel by small increments and convert back, which keeps the hue intact while moving the ratio. A brand color that fails at 12px can often pass at large heading sizes, because large text only needs 3:1. That is why the ratio, not the color, is the design constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Golden Ratio and Proportion
&lt;/h2&gt;

&lt;p&gt;The golden ratio, φ (phi), is the number 1.6180339887…, and it has a distinctive property: the ratio of the whole to the larger part equals the ratio of the larger part to the smaller part. In a line divided at the golden point, the larger segment is about 61.8% of the whole and the smaller about 38.2%. The value satisfies φ = 1 + 1/φ, and it is the positive root of the equation:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;φ&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1.618&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;The Fibonacci sequence produces the same number as a limit: each term is the sum of the previous two, and the ratio of successive terms oscillates around φ, converging on it exactly. This convergence is why designers reach for Fibonacci numbers (1, 1, 2, 3, 5, 8, 13…) as a spacing scale — 8, 13, 21, 34 is close enough to a golden scale that a spacing system built from it inherits the balance without using decimals.&lt;/p&gt;

&lt;p&gt;Famously, φ is used to build balanced rectangles in layout: a golden rectangle's sides are in the ratio 1:φ, and a range of other classic proportions sit nearby.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Proportions&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden (1:φ)&lt;/td&gt;
&lt;td&gt;1.618&lt;/td&gt;
&lt;td&gt;Editorial grids, poster panels, image crops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Square (1:1)&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;Avatars, galleries, product tiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;√2 paper (1:1.414)&lt;/td&gt;
&lt;td&gt;1.414&lt;/td&gt;
&lt;td&gt;ISO A-series print, book margins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fibonacci 3:5&lt;/td&gt;
&lt;td&gt;1.667&lt;/td&gt;
&lt;td&gt;Magazine column pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fibonacci 5:8&lt;/td&gt;
&lt;td&gt;1.600&lt;/td&gt;
&lt;td&gt;Spacing and type scales&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;em&gt;Classic layout proportions plotted by ratio. The golden ratio sits between the square and the √2 paper ratio, near the Fibonacci-derived 5:8 and 3:5 pairs that form practical integer spacing scales.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The golden ratio is best treated as a starting point for composition rather than a requirement. Livio's history of the number cautions against the mythologizing that claims every great work of art uses it — the evidence that the Parthenon or Leonardo's paintings deliberately follow φ is weak, and the number's power is that it appears wherever people have sought balanced proportions, not that it was universally applied. Use it to generate a width from a height (or vice versa) when you need a pleasant rectangle fast; the &lt;a href="https://notacalculator.com/calculator/golden-ratio-calculator" rel="noopener noreferrer"&gt;Golden Ratio Calculator&lt;/a&gt; splits a line or scales a value by φ in either direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Aspect Ratios and Delivery Formats
&lt;/h2&gt;

&lt;p&gt;An aspect ratio is the proportional relationship between an image's width and height, written as two numbers reduced by their greatest common divisor: 1920 × 1080 reduces to 16:9. It answers a delivery question the golden ratio does not — not "does this feel balanced" but "what shape is this frame, and what else will it fit in."&lt;/p&gt;

&lt;p&gt;Delivery formats are strict. A 16:9 video shown on a 21:9 monitor is letterboxed; a 21:9 film on a 16:9 screen is pillarboxed. Photographers crop for print sizes (4:3, 3:2), video and displays standardized on 16:9, and ultrawide monitors push 21:9. Each one is a hard constraint on the canvas, and knowing the target ratio before you start composing saves the most work. RED's filmmaking reference is blunt about this: choose the frame first, then light and compose for it — a ratio is a contract with the viewer.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://notacalculator.com/calculator/aspect-ratio-calculator" rel="noopener noreferrer"&gt;Aspect Ratio Calculator&lt;/a&gt; handles both directions of the problem: it reduces any width and height to its simplest ratio, and it finds the missing dimension when you know the target ratio and one side. It shares DNA with the &lt;a href="https://notacalculator.com/calculator/ratio-calculator" rel="noopener noreferrer"&gt;Ratio Calculator&lt;/a&gt;, which expresses the same relationship as a fraction for pure math work — the aspect calculator adds the display framing (common names, decimals, percentages) that layout work needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Tools at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it computes&lt;/th&gt;
&lt;th&gt;Typical question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contrast Checker&lt;/td&gt;
&lt;td&gt;WCAG contrast ratio, AA/AAA pass/fail&lt;/td&gt;
&lt;td&gt;"Is this text readable on this background?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golden Ratio Calculator&lt;/td&gt;
&lt;td&gt;φ split and scale of any length&lt;/td&gt;
&lt;td&gt;"What width feels balanced for this height?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aspect Ratio Calculator&lt;/td&gt;
&lt;td&gt;Reduced W:H, missing dimension, common name&lt;/td&gt;
&lt;td&gt;"What height fits 1920 px wide at 16:9?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color Converter&lt;/td&gt;
&lt;td&gt;Hex, RGB, and HSL conversions&lt;/td&gt;
&lt;td&gt;"What lightness keeps this hue accessible?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The four tools form a small workflow. Convert a brand color with the Color Converter, check the pair with the Contrast Checker, size the frame with the Aspect Ratio Calculator, and refine the balance with the Golden Ratio Calculator. Each tool hands its output to the next, and none of them alone decides whether a layout works.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Worked Example: A Call-to-Action Card
&lt;/h2&gt;

&lt;p&gt;A concrete walkthrough shows how the four tools cooperate. Suppose you are designing a call-to-action card for a landing page: a headline, a body paragraph, and a button, on a solid background.&lt;/p&gt;

&lt;p&gt;Start with the shape. The card will sit beside a 16:9 hero image, but the card itself should feel calmer and more editorial — a 3:2 frame is a classic choice. You want the card to be 600 px wide. The &lt;a href="https://notacalculator.com/calculator/aspect-ratio-calculator" rel="noopener noreferrer"&gt;Aspect Ratio Calculator&lt;/a&gt; gives the matching height: 600 ÷ 3 × 2 = &lt;strong&gt;400 px&lt;/strong&gt;. That frame now composes cleanly with the hero, sharing its 16:9 language through the common factor of the 3:2 crop.&lt;/p&gt;

&lt;p&gt;Next, the colors. Your brand gray, #767676, on the white card background is a common "muted" choice. The &lt;a href="https://notacalculator.com/calculator/contrast-checker-calculator" rel="noopener noreferrer"&gt;Contrast Checker&lt;/a&gt; reports a ratio of &lt;strong&gt;4.54:1&lt;/strong&gt; — just above the 4.5:1 AA threshold for normal text, but below the 7:1 AAA threshold. That is a defensible result for a landing page: AA passes for body copy, and the requirement is met. If this were a long-form article where AAA is the target, you would darken the gray toward #555555 (7.46:1) to clear the stricter bar.&lt;/p&gt;

&lt;p&gt;Then the spacing. You set the padding and the gap between headline, paragraph, and button using a Fibonacci scale: 8, 13, 21, 34. The &lt;a href="https://notacalculator.com/calculator/golden-ratio-calculator" rel="noopener noreferrer"&gt;Golden Ratio Calculator&lt;/a&gt; can refine the choice — split the 600 px width in the golden ratio and you get &lt;strong&gt;371 px&lt;/strong&gt; for the larger part and &lt;strong&gt;229 px&lt;/strong&gt; for the smaller. A two-column card with the text block at 371 px and the visual at 229 px inherits the golden proportion, while the integer Fibonacci gaps keep the vertical rhythm mechanical and consistent.&lt;/p&gt;

&lt;p&gt;Finally, the button. Its background must clear the 3:1 threshold against the white card because it is a UI component. The &lt;a href="https://notacalculator.com/calculator/color-converter" rel="noopener noreferrer"&gt;Color Converter&lt;/a&gt; converts your brand color into HSL so you can raise its lightness without drifting hue, and the &lt;a href="https://notacalculator.com/calculator/contrast-checker-calculator" rel="noopener noreferrer"&gt;Contrast Checker&lt;/a&gt; re-verifies the new pair. The card is now measurable at every step: a 3:2 frame, a 4.54:1 text ratio, a golden horizontal split, and a 3:1+ component ratio. None of these numbers replaced design judgment — they made the judgment auditable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treating a pass/fail contrast check as the whole accessibility story.&lt;/strong&gt; Contrast is the measurable core, but readability also depends on type size, line length, spacing, and target size for touch. A 4.5:1 pass with 10px gray text is still hard to read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chasing AAA everywhere.&lt;/strong&gt; AAA on body text is a strong goal for long-form reading, but it is not required everywhere. A badge or a disabled button at 2.5:1 is acceptable in many designs; spending hours forcing every element to 7:1 usually produces flat, low-contrast-looking pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applying the golden ratio as a law.&lt;/strong&gt; Compositional rules are heuristics. If a 1.618 ratio makes a panel too wide for its content, a 4:3 or 5:8 frame is a legitimate choice. The ratio suggests, it does not mandate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking contrast on the design canvas, not the delivery frame.&lt;/strong&gt; A hero image cropped to 4:5 on social media may show the text over a different background region than the 16:9 desktop mockup. Verify the pair against the actual export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming all ratios are alike.&lt;/strong&gt; 21:9 ultrawide panels are often physically 43:18 or 64:27. Enter the real resolution, not the marketing label, when you compute the ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the browser check.&lt;/strong&gt; Every contrast tool computes the same ratio, but rendering differences, fonts, and anti-aliasing affect perceived contrast. Spot-check pass/fail pairs in a real browser before shipping.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Frequently Asked Questions
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between color contrast and an aspect ratio?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Color contrast is a luminance relationship between two colors, expressed as a ratio from 1:1 to 21:1, that determines text readability. An aspect ratio is a width-to-height relationship of a frame, like 16:9. Contrast answers whether something can be read; aspect ratio answers what shape the frame is.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What does WCAG contrast ratio 4.5:1 mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It is the minimum contrast ratio WCAG AA requires for normal-size text. It means the lighter color is at least 4.5 times brighter (luminance plus 0.05) than the darker color. Text meeting 4.5:1 is readable for the majority of people with low vision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I compute the contrast ratio between two colors?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Convert each color to its relative luminance L by linearizing the sRGB channels and weighting them 0.2126R + 0.7152G + 0.0722B. Then divide the lighter luminance plus 0.05 by the darker luminance plus 0.05. The Contrast Checker does all of this automatically.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is the golden ratio the same as the Fibonacci sequence?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;No, but they are related. The ratio of consecutive Fibonacci numbers (1, 1, 2, 3, 5, 8, 13…) approaches the golden ratio 1.618 as the sequence grows. The sequence gives an integer approximation of φ that designers use for spacing scales.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How is the golden ratio used in layout design?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Most often to generate a balanced rectangle: multiply one side of a frame by 1.618 (or by its reciprocal 0.618) to get the other. It is also the basis of Fibonacci spacing scales and classic editorial grids. It is a guide for balance, not a compliance requirement.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the most common aspect ratio for displays?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;16:9 is the global standard for HDTV, most online video platforms, smartphones, and computer monitors. Other common ratios are 4:3 (older monitors and photography), 3:2 (camera sensors), 21:9 (ultrawide monitors), and 1:1 (square social media images).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I find the height given a width and a ratio?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Divide the width by the ratio's width component, then multiply by the ratio's height component. For example, 1920 px wide at 16:9 is 1920 ÷ 16 × 9 = 1080 px. The Aspect Ratio Calculator does this for any ratio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do WCAG contrast rules apply to images and logos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Large logos and incidental imagery are exempt from most WCAG contrast requirements, but text overlays on images are not. Any text that carries information — including on hero images, badges, and video captions — must meet the ratio for its size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can a design pass contrast but still be inaccessible?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Yes. Contrast is necessary but not sufficient. Type size, line height, spacing, color-blindness-safe palettes, focus indicators, and motion reduction all affect accessibility. A 4.5:1 pass with a cramped line height and tiny type is still a poor experience.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the fastest way to fix a failing contrast ratio?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Convert the color to HSL and lower the lightness of the background or raise the lightness of the text — or simply darken text and lighten background a few steps at a time, rechecking after each change. Moving to a bolder or larger type also lowers the required threshold from 4.5:1 to 3:1 for large text.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/design-tools-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>guide</category>
      <category>beginners</category>
      <category>design</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Tokens per Second Benchmarks Explained: What You're Actually Measuring</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Wed, 19 Aug 2026 03:51:59 +0000</pubDate>
      <link>https://dev.to/apeder/tokens-per-second-benchmarks-explained-what-youre-actually-measuring-47hl</link>
      <guid>https://dev.to/apeder/tokens-per-second-benchmarks-explained-what-youre-actually-measuring-47hl</guid>
      <description>&lt;p&gt;&lt;em&gt;What tok/s really measures, how concurrency changes it, and why a single-user benchmark is not the whole story for local LLM performance.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A Few Moments Later… How Fast Is "Fast"?
&lt;/h2&gt;

&lt;p&gt;Every interface in the world of local AI eventually shows you that dreaded spinner, and on the wrong setup it sits there long enough that your brain supplies the meme: &lt;strong&gt;"A few moments later…"&lt;/strong&gt; That pause is a number wearing a disguise. Somewhere inside your machine, the model is grinding out tokens — fragments of words — and the only question that matters is how many of them it produces per second.&lt;/p&gt;

&lt;p&gt;Tokens per second (tok/s) is the universal speedometer of local LLMs, quoted in every benchmark and every GPU review. But it is also one of the most misleading numbers in the field, because the same model can measure 45 tok/s or 793 tok/s depending on how you test it. This guide explains what the number actually means, why it moves so dramatically, and how to read a benchmark without fooling yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Token Actually Is
&lt;/h2&gt;

&lt;p&gt;Before speed makes sense, the unit has to. Models do not read words; they read tokens, which are chunks of text roughly three-quarters of a character on average in English. The word "calculator" might be one token or three, depending on the tokenizer, and this is not idle trivia — it is the reason the same prompt can cost a different amount across providers, as the &lt;a href="https://notacalculator.com/calculator/token-counter-calculator" rel="noopener noreferrer"&gt;Token Counter Calculator&lt;/a&gt; shows in practice.&lt;/p&gt;

&lt;p&gt;Because tokens are the unit of both billing and speed, "tokens per second" is the single number that connects all three corners of the local AI decision: how fast the model answers (tok/s), how big the model is (parameters), and what it costs to run (hardware amortized over time). A model doing 50 tok/s reads roughly 100-150 words per second — comfortably faster than you can read. A model stuck at 5 tok/s feels like a slow internet connection in 1998.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Single-User Number Is Not the Whole Story
&lt;/h2&gt;

&lt;p&gt;Here is the trap: most consumer benchmarks report tok/s at one user, one request. That number describes a private chat with your own model, and for that scenario it is the right metric. But the moment you put a model behind an API or a team, the picture changes entirely, because serving engines handle many requests at once and the interesting number becomes &lt;em&gt;throughput&lt;/em&gt; — total tokens produced across all users per second.&lt;/p&gt;

&lt;p&gt;The mechanism that changes everything is &lt;strong&gt;continuous batching&lt;/strong&gt;. A naive server waits for one request to finish before starting the next. A batched server fills idle GPU cycles with other requests, keeping the silicon busy. This is why vLLM's key innovation — continuous batching plus PagedAttention — lets one GPU serve dramatically more total traffic than a tool that processes requests one at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Concurrency Curve: Back Street vs Highway
&lt;/h2&gt;

&lt;p&gt;Think of it like driving. At one car, a quiet back street and a six-lane highway are both quick — there is no traffic, so lane count does not matter. That is the single-user case, where Ollama and vLLM are genuinely close, with Ollama even slightly ahead on some hardware.&lt;/p&gt;

&lt;p&gt;Now imagine rush hour. The back street gridlocks at a handful of cars; the highway keeps absorbing lane after lane of traffic. That is the concurrency curve for local inference. At a single request both tools idle along, but as concurrency climbs, the batching engine pulls ahead dramatically:&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Throughput vs concurrency for Llama 3.1 8B on A100 40GB (Red Hat benchmark pattern). Single user: Ollama ahead. Heavy concurrency: vLLM up to ~19x total throughput.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Under sustained load, the Red Hat benchmark on an A100 40GB with Llama 3.1 8B measured vLLM peaking around 793 tok/s of combined throughput versus about 41 tok/s for Ollama — roughly a &lt;strong&gt;19x gap&lt;/strong&gt; that emerged only as concurrency rose from 1 to 256 users. Ollama even stayed behind when told to run 32 parallel workers. The exact multiplier varies by model, GPU, and workload — it is not a universal constant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Throughput vs Latency: Two Numbers, Two Questions
&lt;/h2&gt;

&lt;p&gt;The tok/s number hides a second, equally important split: &lt;strong&gt;throughput&lt;/strong&gt; versus &lt;strong&gt;latency&lt;/strong&gt;. Throughput is how much work the machine does per second (useful for a server). Latency is how long one specific user waits for their answer (useful for a chat). They pull in opposite directions — serving more users at once raises throughput but can stretch how long any single request takes to start producing.&lt;/p&gt;

&lt;p&gt;That is why real serving guides pay attention to the &lt;strong&gt;99th-percentile latency (P99)&lt;/strong&gt;, the wait experienced by the slowest 1% of requests. In the same Red Hat test, vLLM delivered an 80-millisecond P99 under concurrency while Ollama's tail latency ballooned to 673 milliseconds — an 8x difference that matters enormously for interactive applications, even though average latencies looked acceptable. Average numbers flatter the tool that fails occasionally; percentiles expose it.&lt;/p&gt;

&lt;p&gt;For a single user running a private model, latency and throughput are almost the same number, and the headline tok/s figure is honest enough. For a server, ignore the single-user benchmark entirely — read the concurrency curve and the P99 column.&lt;/p&gt;

&lt;p&gt;There is a third metric hiding between them: &lt;strong&gt;time to first token (TTFT)&lt;/strong&gt;, the delay between submitting a prompt and the first word appearing. TTFT is dominated by prompt processing — the model reading and attending to your entire input before it can start generating. A long RAG context or a multi-turn history makes TTFT grow, independently of how fast the model then generates subsequent tokens. Under concurrency, TTFT is where a poorly batched server shows its worst face, because queued requests wait behind others before their prompt is even processed.&lt;/p&gt;

&lt;p&gt;The three numbers tell different stories. Tok/s describes sustained generation, P99 describes worst-case wait under load, and TTFT describes how the experience &lt;em&gt;starts&lt;/em&gt;. A model can look great on all three for a chat and collapse on TTFT for a long-document workload, or vice versa. When you read a benchmark, ask which of the three it is actually reporting — many reviews only quote tok/s and quietly ignore the other two, which is exactly where the misleading numbers hide.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Realistic Speeds Look Like in 2026
&lt;/h2&gt;

&lt;p&gt;Consumer hardware in 2026 lands in predictable bands, and knowing them keeps expectations sane. A 7B model in Q4 on a fast gaming card typically generates in the 120-300 tok/s range, which is more than fast enough for interactive chat. A 70B model on the same hardware crawls to single digits or low tens, because the memory bandwidth and compute demand scale with size. Unified-memory Macs sit in the middle: large models fit easily, but generation speed is capped by their bandwidth ceiling.&lt;/p&gt;

&lt;p&gt;The practical translation: for a personal assistant or coding autocomplete, anything above roughly 30-40 tok/s feels instant, and the difference between 80 and 200 tok/s is barely noticeable to a human. The tok/s arms race only matters when you are serving many users, where the concurrency curve decides whether your hardware keeps up at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Speed Feels Like: Tokens to Real Words
&lt;/h2&gt;

&lt;p&gt;Numbers on a screen are abstract until you translate them into the experience of actually waiting. Because a token is roughly three-quarters of a word, you can estimate reading speed by multiplying tok/s by 0.75 to get words per second, then by 60 to get words per minute. The result is a feeling you already have a reference for.&lt;/p&gt;

&lt;p&gt;At 50 tok/s, a model produces about 2,250 words per minute — several times faster than any human reads. Long answers materialize almost instantly, and the only real pause is the initial delay before the first token, which is dominated by prompt processing rather than generation speed. This is the experience most people mean when they say local AI "feels fast enough."&lt;/p&gt;

&lt;p&gt;Drop to 10 tok/s and the math becomes 450 words per minute. Still readable, but now you watch the text assemble across a couple of seconds, and a 500-word answer takes nearly a minute. The "A few moments later…" feeling starts here, not because the model is slow in absolute terms, but because it is slower than your patience.&lt;/p&gt;

&lt;p&gt;The distinction between these two experiences is almost never the headline benchmark. A review that says "50 tok/s" tells you nothing about whether the first token arrived in 200 milliseconds or 4 seconds — and that first-token latency is what you actually notice when you hit Enter. When you test a model yourself, watch the gap between pressing Enter and seeing the first word, then judge the generation rate separately. Both matter, but they are different problems with different fixes: prompt processing is about compute and prompt length, while generation speed is about memory bandwidth.&lt;/p&gt;

&lt;p&gt;A useful mental anchor: compare the model's reading speed against your own. If a model produces text faster than you can read it, generation speed is effectively a non-issue — your attention is the bottleneck, not the hardware. That threshold sits near 15-20 tok/s for a comfortable reader, which is why budget local setups on 7B models feel so satisfying and why a slow 70B model on the wrong card feels broken even when it is "technically working." Speed, in the end, is a relationship between the machine and the person waiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Measure Your Own Tokens per Second
&lt;/h2&gt;

&lt;p&gt;You do not need a lab to get your number — just ten minutes and a model file. The two most common tools, Ollama and vLLM, both make measurement straightforward, and both are open source with their own documentation.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Ollama&lt;/strong&gt; , the fastest path is interactive: download a GGUF model, run &lt;code&gt;ollama run &amp;lt;model&amp;gt;&lt;/code&gt;, and ask it to produce a fixed amount of text. The terminal prints timing statistics when the response finishes, including a tokens-per-second figure for that exact prompt on your exact hardware. Keep the prompt the same across tests and you get a repeatable baseline.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;vLLM&lt;/strong&gt; , the honest number requires a tiny bit of setup because the tool is built for serving, not single chats. Point it at your model file, start the OpenAI-compatible server, and send a request through its &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint with a known prompt length. The response metadata includes timing information you can turn into tok/s, and you can raise concurrency by firing several requests in parallel to build your own concurrency curve.&lt;/p&gt;

&lt;p&gt;Whichever tool you use, standardize three things so the number is comparable: the &lt;strong&gt;model&lt;/strong&gt; (same quantization), the &lt;strong&gt;prompt length&lt;/strong&gt; (same token count), and the &lt;strong&gt;hardware&lt;/strong&gt; (same GPU and driver). Change any one and the benchmark changes with it. Also measure a couple of times — GPU thermals and background load shift first-run numbers by 10-20%.&lt;/p&gt;

&lt;p&gt;Once you have your real tok/s, plug your model choice and hardware into the &lt;a href="https://notacalculator.com/calculator/hardware-requirements-calculator" rel="noopener noreferrer"&gt;LLM Hardware Requirements Calculator&lt;/a&gt; to sanity-check that the setup makes sense before committing to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Tips for Reading Benchmarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask "at what concurrency?"&lt;/strong&gt; A tok/s number without a concurrency context is meaningless. 45 tok/s at one user and 45 tok/s at 50 users are opposite results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the model to your hardware.&lt;/strong&gt; Benchmarks on A100s do not predict RTX 5090 performance. Find numbers for your actual card and quantization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the P99, not the average, for serving.&lt;/strong&gt; Average latency hides the requests that actually time out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read your own tok/s before buying.&lt;/strong&gt; Download a GGUF, run the &lt;a href="https://notacalculator.com/calculator/hardware-requirements-calculator" rel="noopener noreferrer"&gt;LLM Hardware Requirements Calculator&lt;/a&gt; for the model+context combo, then measure the real speed on your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not pay for more speed than you can perceive.&lt;/strong&gt; Single-user chat above ~40 tok/s is already instant. The budget is better spent on a bigger model than a faster small one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the numbers disagree, trust the method.&lt;/strong&gt; A benchmark that does not state its concurrency, GPU, quantization, and context is marketing, not measurement.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations and Edge Cases
&lt;/h2&gt;

&lt;p&gt;Tokens per second is a snapshot, not a law. Real numbers shift with context length (longer prompts slow generation), temperature sampling (which changes how much the model reuses cache), quantization, and the exact runtime. Batch efficiency also depends on request mix — varied prompt lengths batch better than uniform ones. And tok/s is silent about quality: a blazing-fast small model is still a small model. Use it to compare how well a given model runs on a given box, not to compare models against each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How many tokens per second is 'good' for a local LLM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;For interactive chat, roughly 30-40 tok/s already feels instant to most people. Above that, gains are barely perceptible. For coding autocomplete, similar thresholds apply. Only servers need the 500+ tok/s numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why is my model slower than the benchmark said?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Benchmarks use specific hardware, quantization, and context lengths. Longer context, a bigger model, a lower-bandwidth card, or background processes all cut real speed. Measure your own setup rather than trusting a headline number.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does vLLM beat Ollama under concurrency but not at one user?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Continuous batching. At a single request there is nothing to batch, so Ollama's simpler pipeline can even edge ahead. As requests pile up, vLLM fills idle GPU cycles with other work, multiplying total throughput.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is a 70B model always slower than a 7B?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Roughly yes on the same hardware, because every token pass touches more parameters. The gap is often 10x or more. This is why fitting a big model matters less than fitting one that generates at a usable speed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the difference between throughput and latency?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Throughput is total tokens produced per second across all users (a server metric). Latency is how long one user waits for their response (an experience metric). Serving more users raises throughput but can worsen tail latency.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use tok/s to compare two different models?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Only loosely. Tokens are tokenizer-dependent, so two models can split the same text into different token counts. Tok/s compares how efficiently a model runs on a machine, not how good the model is.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/local-llm-tokens-per-second-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>guide</category>
      <category>ai</category>
      <category>tokens</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Estimate Local LLM Cost per Token</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:48:20 +0000</pubDate>
      <link>https://dev.to/apeder/how-to-estimate-local-llm-cost-per-token-1a1p</link>
      <guid>https://dev.to/apeder/how-to-estimate-local-llm-cost-per-token-1a1p</guid>
      <description>&lt;p&gt;Hardware amortization, electricity, and tokens per second translated into a real cost per token — compared against 2026 cloud API pricing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pied Piper Trap: When Your Server Costs More Than It Makes
&lt;/h2&gt;

&lt;p&gt;In &lt;em&gt;Silicon Valley&lt;/em&gt;, Richard Hendricks builds a brilliant compression algorithm that eventually brings down his own company's infrastructure — the deeper he pushes, the more the hardware bill balloons while the product fights to justify it. It is fiction, but every developer who has self-hosted an LLM has lived a smaller version: the GPU sits in the corner glowing, the electricity meter spins, and somewhere in the back of your mind is the question &lt;em&gt;"am I saving money or just paying a different bill?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The honest answer requires a number most people never compute: &lt;strong&gt;the cost per token of your local setup.&lt;/strong&gt; Cloud APIs advertise prices per million tokens; local hardware comes with a price tag but no per-token figure. Until you translate one into the other, "local is cheaper" and "local is expensive" are both vibes, not facts. This guide builds the math from first principles so you can answer the question for your own machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Local Cost per Token Formula
&lt;/h2&gt;

&lt;p&gt;The cost of a local token has three components, all of which you can estimate before buying anything:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;cost/token&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tokens&amp;nbsp;generated&amp;nbsp;per&amp;nbsp;period&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;hardware&amp;nbsp;amortization&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;electricity&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;operational&amp;nbsp;overhead&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Each piece matters differently depending on your setup. Hardware amortization dominates for a new GPU. Electricity dominates for a machine that runs 24/7. Operational overhead — your time, maintenance, cooling, storage — is the piece everyone forgets and the one that quietly makes self-hosting expensive for a small team.&lt;/p&gt;

&lt;p&gt;The crucial insight is that the denominator grows with &lt;em&gt;utilization&lt;/em&gt;. A GPU that generates a million tokens a month spreads its fixed cost across a million tokens; the same GPU generating ten thousand spreads the same fixed cost across ten thousand. This is why "local is cheap" and "local is expensive" are both true — the answer depends entirely on how much you actually use the hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Amortizing the Hardware
&lt;/h2&gt;

&lt;p&gt;Hardware is a fixed cost, so the way to price it is amortization: spread the purchase price over the useful life of the machine. The standard assumption for consumer GPU hardware is 36 months — long enough that the cost-per-month is meaningful, short enough that you are not pretending a GPU lasts forever.&lt;/p&gt;

&lt;p&gt;A real example makes this concrete. An RTX 5090 lists at $1,999 MSRP, though street prices run higher. Amortized over 36 months at the MSRP, that is about $56 per month before electricity. If that card generates, say, 40 million tokens per month (a heavy but realistic single-user workload on a fast model), the hardware alone contributes roughly $1.40 per million tokens.&lt;/p&gt;

&lt;p&gt;The formula for hardware per-token cost is simple:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;hardware&amp;nbsp;cost/token&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;tokens&amp;nbsp;per&amp;nbsp;month&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;price&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;/&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;months&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;If your usage is a tenth of that — 4 million tokens a month — the same card costs $14 per million tokens, suddenly more than many cloud APIs. The fixed cost does not care how much you use it; that is the whole trap.&lt;/p&gt;

&lt;p&gt;Two refinements make the amortization honest. First, subtract a &lt;strong&gt;resale value&lt;/strong&gt; : a 36-month-old consumer GPU still sells for a meaningful fraction of its original price, so the true cost is the purchase price minus what you recover at the end, spread over the months you actually own it. Second, remember that &lt;strong&gt;scaling means buying hardware again&lt;/strong&gt; : moving from a 7B to a 70B model may mean a second card or a workstation, which resets the amortization clock and roughly doubles the fixed cost overnight. Amortization is not a one-time decision; it is a commitment that renews every time your requirements grow.&lt;/p&gt;

&lt;p&gt;The deeper point is that hardware cost is a &lt;em&gt;step function&lt;/em&gt; while cloud cost is a &lt;em&gt;curve&lt;/em&gt;. Your local setup has discrete cost jumps every time you add a GPU or a bigger machine, and between those jumps the per-token cost keeps falling as you generate more. The cloud has no steps — the price per token is flat whether you send one request or a million. That difference in shape is why local favors heavy, growing usage and cloud favors light, variable usage, and why the two never converge into one clean answer.&lt;/p&gt;

&lt;p&gt;A useful discipline is to re-run the estimate on a fixed schedule — monthly, or every time your usage visibly changes. Hardware prices, electricity rates, cloud pricing, and your own workload all drift over time, and the answer to "is local worth it?" drifts with them. A setup that was clearly worth it in March can quietly become a money pit by September, or vice versa, with no single event marking the flip. Treating the estimate as a living number, not a one-time calculation, is the difference between making a decision and pretending a decision stays made.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Electricity Bill Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;Electricity is the variable cost that scales with actual runtime, and it is the piece most cost estimates skip entirely. The formula is straightforward:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;electricity/month&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;watts&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;hours&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;rate&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;An RTX 5090 has a 575W thermal design power. Add the rest of the system — CPU, motherboard, fans — and a loaded inference box commonly draws 700-900W at the wall. At a 15 cents per kWh rate, running that box at full load for 12 hours a day lands near $40-55 a month. That is often &lt;em&gt;more&lt;/em&gt; than the hardware amortization, and it is pure recurring cost that never goes away.&lt;/p&gt;

&lt;p&gt;This is why the electricity component punishes always-on setups. A GPU that idles between requests still draws a baseline; a server that must stay responsive 24/7 pays the full tab even if you use it an hour a day. Cloud APIs, by contrast, charge only for the tokens you actually send — the idle time is free because it is someone else's electricity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together: Your Real Cost per Token
&lt;/h2&gt;

&lt;p&gt;Combining the pieces for the same RTX 5090 at heavy usage gives a realistic picture. Hardware amortization contributes about $1.40 per million tokens, electricity another $1.00-1.40 depending on utilization, and operational overhead (your time, setup, maintenance) adds whatever you value your hours at. The honest all-in number for a well-utilized single-GPU setup lands in the range of $2.50-4 per million tokens — before you count your own labor.&lt;/p&gt;

&lt;p&gt;Now compare that to cloud pricing in 2026. DeepSeek V4 Flash costs $0.14 per million input tokens and $0.28 per million output. Claude Sonnet 5 runs $2 per million input and $10 per million output, and GPT-5.6 tiers span a wide range depending on the model.&lt;/p&gt;

&lt;p&gt;The pattern is the key takeaway: a &lt;strong&gt;well-utilized local box lands near the mid-tier cloud pricing&lt;/strong&gt; , competitive with Sonnet 5 and far below the flagship tiers, but dramatically more expensive than the cheapest cloud models like DeepSeek Flash. If your workload can tolerate a smaller model, the cloud at $0.14/M is simply unbeatable — local hardware cannot touch that price per token no matter how hard you amortize it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloud Pricing as the Benchmark You Cannot Ignore
&lt;/h2&gt;

&lt;p&gt;Local cost only means something against a reference, and that reference is what the same tokens cost as an API call. The 2026 cloud market spans two orders of magnitude, which is exactly why a single "is local cheaper?" answer does not exist. The range, with verified August 2026 pricing, looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input $/1M&lt;/th&gt;
&lt;th&gt;Output $/1M&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;V4 Flash&lt;/td&gt;
&lt;td&gt;$0.14&lt;/td&gt;
&lt;td&gt;$0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;V4 Pro&lt;/td&gt;
&lt;td&gt;$0.435&lt;/td&gt;
&lt;td&gt;$0.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;GPT-5.6 Luna&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;GPT-5.6 Terra&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$12.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Claude Fable 5&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;$50.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;em&gt;Input price per million tokens for representative 2026 models. The gap between budget and flagship is an order of magnitude — and local hardware must compete against the whole range.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The way to use this table is to pick the cloud model closest to what you would run locally. If you would self-host a compact 7B model, the honest comparison is against DeepSeek Flash at $0.14/M input — and local loses, hard. If you would self-host a top-tier 70B, the comparison is against Claude Sonnet 5 at $2/M, where a well-utilized local box can genuinely compete.&lt;/p&gt;

&lt;p&gt;This is why "local is cheaper" debates are always underspecified. The real question is &lt;em&gt;cheaper than which model, at what utilization?&lt;/em&gt; Until you pin down both, you are comparing a number you have not computed against a range you have not scoped.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Local Actually Wins the Cost Game
&lt;/h2&gt;

&lt;p&gt;The numbers above reveal that local is rarely the cheapest option on pure cost-per-token. Where it wins is in the situations where the price per token is not the whole equation. Three cases justify self-hosting despite the math:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy and data control.&lt;/strong&gt; If your prompts contain sensitive data you do not want to send to an API provider, the cost of local is the price of keeping the data in the building. That is a compliance and risk decision, not a per-token optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictable heavy usage.&lt;/strong&gt; If you genuinely generate tens of millions of tokens a month on a mid-tier model, the fixed hardware cost gets spread thin enough to undercut the equivalent cloud model — the break-even analysis in the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt; finds your exact crossover point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency and availability control.&lt;/strong&gt; A local model has no network hop, no rate limits, and no provider outages. For applications where a few hundred milliseconds matter or where reliability is contractual, that has real value even if the token price is higher.&lt;/p&gt;

&lt;p&gt;In every other case — light usage, bursty demand, budget-conscious projects, or workloads that only need a small model — the cloud wins on cost. The discipline is to compute your own numbers instead of assuming.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Step-by-Step Cost Estimate
&lt;/h2&gt;

&lt;p&gt;Here is the process you can run today, without buying anything. First, decide your realistic monthly token volume — count your actual usage with the &lt;a href="https://notacalculator.com/calculator/token-counter-calculator" rel="noopener noreferrer"&gt;Token Counter Calculator&lt;/a&gt; rather than guessing. Second, price the hardware you are considering (card, PSU, RAM, storage) and divide by your amortization period in months. Third, estimate electricity from the card's power draw and your expected runtime. Fourth, add a line for operational overhead — your hours have a value even if you do not bill them. Finally, divide the monthly total by your monthly token volume to get cost per token, and compare it against the cloud price for the same class of model using the &lt;a href="https://notacalculator.com/calculator/llm-api-cost-calculator" rel="noopener noreferrer"&gt;LLM API Cost Calculator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The single most common mistake is skipping step one and assuming a usage level. Run the numbers for both a light month and a heavy month — the spread will tell you whether local is a rounding error or a liability. Most people discover that local is either obviously worth it or obviously not, and the grey zone is narrower than the marketing suggests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Tips for Cost Estimation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amortize over 36 months, not "forever."&lt;/strong&gt; GPUs become obsolete faster than they physically fail. A 36-month window is the honest middle ground.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count electricity at the wall, not the TDP.&lt;/strong&gt; Add 20-40% to the card's rated power to cover the system around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include your time.&lt;/strong&gt; Setup, updates, and debugging are real costs. If your hours are worth $50, a week of tinkering is $400 of operational overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recompute at your real volume.&lt;/strong&gt; The break-even point lives at the intersection of usage and cost. Guess the volume wrong and every downstream number is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember the cloud has no idle cost.&lt;/strong&gt; For bursty or unpredictable demand, paying per token beats paying per month for hardware you do not saturate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not forget resale.&lt;/strong&gt; A 36-month amortization can subtract a partial resale value at the end, which many people ignore and which makes local a bit cheaper in hindsight.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations and Edge Cases
&lt;/h2&gt;

&lt;p&gt;The per-token estimate is only as good as its assumptions, and several assumptions are easy to get wrong. Electricity rates vary wildly by region and time-of-day; a card running at idle draws far less than its peak TDP, so always-on setups may cost less — or more — than the naive formula. Token volume is tokenizer-dependent: the same workload produces different token counts across models, which silently changes the per-token comparison. And comparing local against cloud output-only pricing misses the reality that real API bills mix cheap input and expensive output. Finally, the largest unquantified term is usually your own labor — it does not appear in the formula unless you add it, but it is often the difference between local being a bargain and local being a hobby.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is running a local LLM cheaper than cloud APIs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Only under heavy, predictable usage. A well-utilized box lands near mid-tier cloud pricing, but the cheapest cloud models (like DeepSeek at $0.14/M input) beat local hardware on pure price per token. Local wins on privacy, control, and latency, not usually on raw cost.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How much does it cost to run an RTX 5090 locally per month?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Roughly $56 in amortization plus $40-55 in electricity at 12h/day, before your labor. That translates to roughly $2.50-4 per million tokens at heavy utilization — competitive with mid-tier cloud, far above the cheapest cloud models.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the break-even point for local vs cloud?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It depends on your model choice, hardware, and volume. Against Claude Sonnet 5 the crossover is around 135M tokens a month; against cheap DeepSeek pricing it may be unreachable on a single consumer GPU. Use the Local LLM Break-Even Calculator for your specific numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why is local LLM cost per token never quoted by vendors?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Because it depends entirely on your utilization, electricity rate, and hardware. There is no fixed per-token price for hardware, so nobody can quote one honestly. You have to compute it from your own numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does local cost less if I only use it occasionally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Almost never. Occasional use leaves the fixed hardware and electricity costs spread over very few tokens, which pushes cost per token far above cloud rates. Light users should strongly prefer pay-per-token cloud APIs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I count my own labor in the cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Estimate the hours spent on setup, updates, and maintenance, value them at whatever your time is worth, and divide by your monthly token volume. It is often the largest hidden term and the one that turns 'free' self-hosting into a real expense.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/local-llm-cost-per-token-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>llm</category>
      <category>tokens</category>
    </item>
    <item>
      <title>How Much VRAM Do You Really Need for Local LLMs?</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Sun, 16 Aug 2026 15:11:56 +0000</pubDate>
      <link>https://dev.to/apeder/how-much-vram-do-you-really-need-for-local-llms-142n</link>
      <guid>https://dev.to/apeder/how-much-vram-do-you-really-need-for-local-llms-142n</guid>
      <description>&lt;p&gt;The VRAM equation explained: quantized model sizes, real 2026 GPU options, and the 'can I run it?' answer for any local model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can You Run It? The Question Every Gamer Already Knows
&lt;/h2&gt;

&lt;p&gt;Every PC gamer knows the feeling: you open a store page, scroll to the system requirements, and hold your breath. &lt;em&gt;"Can I run it?"&lt;/em&gt; The GPU shader count, the RAM floor, the dreaded minimum versus recommended specs. Local AI has inherited that exact anxiety, repackaged as a single metric: &lt;strong&gt;VRAM&lt;/strong&gt;. When you read about a 70B model running "locally," the unspoken question is whether your graphics card actually has the memory to hold it.&lt;/p&gt;

&lt;p&gt;The good news is that the math is refreshingly simple — more predictable than game performance, which depends on drivers, resolution, and scene complexity. An LLM either fits in your VRAM or it does not, and the calculation is a straightforward equation you can do on a napkin. This guide teaches you that equation, walks through the real numbers for 2026 hardware, and helps you answer the &lt;em&gt;"can I run it?"&lt;/em&gt; question for any model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The VRAM Equation in Plain Terms
&lt;/h2&gt;

&lt;p&gt;Every model consumes memory proportional to its parameters, and that number depends on how you store the weights. The core rule of thumb:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;VRAM&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;parameters&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;bits&amp;nbsp;per&amp;nbsp;weight&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;This gives you gigabytes from a parameter count. At 16-bit precision, a 7B model needs about 14GB of weights alone. That is why almost everyone who runs models locally uses &lt;strong&gt;quantization&lt;/strong&gt; — storing weights in fewer bits (8, 4, or even 3 bits) at a modest quality cost, which is how a 7B model fits into a mainstream 8GB card.&lt;/p&gt;

&lt;p&gt;Quantization is not a lossy afterthought; it is the entire reason local AI is possible on consumer hardware. The 4-bit GGUF format is the de facto standard for local inference, and the numbers below use it as the baseline.&lt;/p&gt;

&lt;p&gt;The precision ladder shows why the bit choice dominates everything else. The same 7B model goes from unfittable on most consumer cards at FP16 to comfortable on an 8GB card at Q4:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Bits/weight&lt;/th&gt;
&lt;th&gt;7B model VRAM&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FP16&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;~14GB&lt;/td&gt;
&lt;td&gt;Reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;~7GB&lt;/td&gt;
&lt;td&gt;Near-lossless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~5.5GB&lt;/td&gt;
&lt;td&gt;Very good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;~4GB&lt;/td&gt;
&lt;td&gt;Good (sweet spot)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;~3GB&lt;/td&gt;
&lt;td&gt;Noticeable loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;em&gt;VRAM needed for a 7B model at each precision level. Quantization is the lever that makes local LLMs practical on consumer GPUs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each step down roughly halves memory while adding a little quantization error. That is why the community standard for local use is Q4: it fits mainstream cards while staying close enough to the original quality that most users cannot tell the difference on everyday tasks. Going below Q4 is only worth it when the alternative is not running the model at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quantized Sizes: The Numbers That Matter
&lt;/h2&gt;

&lt;p&gt;Here is the practical reality for popular model sizes, in 4-bit quantization (the default you will encounter in GGUF files):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;4-bit VRAM (weights)&lt;/th&gt;
&lt;th&gt;Realistic total with context&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;~4GB&lt;/td&gt;
&lt;td&gt;~5-6GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;~4.8GB&lt;/td&gt;
&lt;td&gt;~6GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3&lt;/td&gt;
&lt;td&gt;14B&lt;/td&gt;
&lt;td&gt;~8GB&lt;/td&gt;
&lt;td&gt;~9-10GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3&lt;/td&gt;
&lt;td&gt;70B&lt;/td&gt;
&lt;td&gt;~40GB&lt;/td&gt;
&lt;td&gt;~44-48GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3&lt;/td&gt;
&lt;td&gt;405B&lt;/td&gt;
&lt;td&gt;~200GB+&lt;/td&gt;
&lt;td&gt;Needs multi-GPU or server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "realistic total" column matters more than the raw weights. Your context window, KV cache, and the running application all live in the same memory. A 70B model at Q4 needs roughly 40GB of weights, plus several more gigabytes for a reasonable context window — which is why you will rarely see a 70B running comfortably on a single 24GB card without aggressive quantization or a short context.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 2026 Hardware Landscape
&lt;/h2&gt;

&lt;p&gt;The current consumer flagship is the NVIDIA RTX 5090 with 32GB of GDDR7 memory and 1,792 GB/s of bandwidth. That 32GB is the dividing line: it comfortably handles 7B, 14B, and even 32B models at Q4, but a 70B model only fits at Q3 or smaller — for Q4 70B, you are looking at either two cards or a workstation part.&lt;/p&gt;

&lt;p&gt;The other popular route is unified memory. Apple's Mac Studio M5 Ultra packs up to 192GB of unified memory shared between CPU and GPU, which can run 120B+ parameter models natively that a discrete GPU could not touch. The trade-off is bandwidth: 819 GB/s on the M5 Ultra versus 1,792 GB/s on the 5090, which directly caps tokens-per-second generation speed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hardware&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 4060 Ti&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;288 GB/s&lt;/td&gt;
&lt;td&gt;7B Q4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 4090&lt;/td&gt;
&lt;td&gt;24GB&lt;/td&gt;
&lt;td&gt;1,008 GB/s&lt;/td&gt;
&lt;td&gt;14B-32B Q4, 70B Q3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;32GB&lt;/td&gt;
&lt;td&gt;1,792 GB/s&lt;/td&gt;
&lt;td&gt;32B Q4, 70B Q3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac Studio M5 Ultra&lt;/td&gt;
&lt;td&gt;192GB unified&lt;/td&gt;
&lt;td&gt;819 GB/s&lt;/td&gt;
&lt;td&gt;70B-120B+ native&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Beyond these consumer options there are two practical routes for the 70B-plus tier. The first is multi-GPU: two RTX 4090s give 48GB of combined VRAM, which fits a 70B at Q4 — but you need motherboard support, a high-wattage power supply, and a runtime like vLLM that shards the model across cards. The second is the new class of desktop AI appliances such as the NVIDIA DGX Spark with 128GB of unified memory, which puts a serious fraction of a datacenter node on your desk at $4,699. Each route trades money, complexity, and speed differently; the right choice depends on whether you need one model at scale or many models at convenience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Context Eats VRAM (the Part Nobody Explains)
&lt;/h2&gt;

&lt;p&gt;Almost every guide tells you the weight size and stops there. But the reason your "40GB for a 70B" plan falls apart in practice is the &lt;strong&gt;KV cache&lt;/strong&gt; — memory the model uses to remember what has been said so far while generating. Every token in your prompt and every token the model has produced gets stored in this cache, scaled by the number of attention layers and heads.&lt;/p&gt;

&lt;p&gt;The KV cache is why your usable VRAM shrinks as your conversation grows. A short prompt on a 7B model might use 1-2GB of cache; a long RAG document or a multi-turn chat can eat 4-8GB or more. The formula for cache size is roughly proportional to &lt;em&gt;context length × model size&lt;/em&gt;, which is why the same model feels "fine" at a 2K context and suddenly OOMs at 32K.&lt;/p&gt;

&lt;p&gt;This creates the practical rule that most people learn the hard way: &lt;strong&gt;your model fits, but your conversation does not.&lt;/strong&gt; When you plan VRAM, decide your context budget first, then add it to the weight size. The &lt;a href="https://notacalculator.com/calculator/context-window-calculator" rel="noopener noreferrer"&gt;Context Window Calculator&lt;/a&gt; helps you size realistic context needs for your workload before you buy hardware around the wrong assumption.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Check Your Real VRAM Usage
&lt;/h2&gt;

&lt;p&gt;Theory gets you in the ballpark; measurement gets you the answer. Once you have a model running, you can confirm exactly how much memory it really uses instead of trusting the marketing numbers, and the tools to do it are free and built into your operating system.&lt;/p&gt;

&lt;p&gt;On Windows, open the Task Manager and go to the Performance tab, then GPU. You will see "Dedicated GPU memory" — that is your VRAM. Run your model, start a conversation, and watch the number rise as the prompt grows. The difference between the idle reading and the in-use reading is the model's true footprint, and it will climb as your context window fills, which is the KV-cache effect in action.&lt;/p&gt;

&lt;p&gt;On Linux, the command-line tools do the same job. &lt;code&gt;nvidia-smi&lt;/code&gt; shows a live per-process memory breakdown for NVIDIA cards, while &lt;code&gt;rocm-smi&lt;/code&gt; and &lt;code&gt;radeontop&lt;/code&gt; cover AMD hardware. A simple loop that samples &lt;code&gt;nvidia-smi --query-gpu=memory.used&lt;/code&gt; every few seconds while you chat with the model gives you a clear picture of both the peak and the steady-state usage.&lt;/p&gt;

&lt;p&gt;The practical reason to measure is that the advertised figures and the real numbers rarely match. A "4-bit 7B" model ships weights of roughly 4GB, but the running process regularly holds 5-6GB once the tokenizer, runtime buffers, and a normal conversation are included. Knowing your real ceiling — not the theoretical one — is what turns "should fit" into "does fit." If you find yourself right at the edge, the levers are the same ones from earlier: drop to a smaller quantization, shorten the context budget, or offload layers to system RAM.&lt;/p&gt;




&lt;h2&gt;
  
  
  The VRAM Calculation Walked Through
&lt;/h2&gt;

&lt;p&gt;Let us do the napkin math for a 70B model at Q4, because that is the case that confuses most people. First, the weights:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;weights&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;70&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;35&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;&amp;nbsp;GB&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;That is 35GB just for the weights, before anything else. Add a few gigabytes for the KV cache at a moderate context window, plus the model runtime overhead, and you are at roughly 40-44GB. Against an RTX 5090's 32GB, it does not fit — so the realistic options are Q3 quantization (smaller, some quality loss) or CPU offloading, where part of the model lives in system RAM and only active layers stream through the GPU.&lt;/p&gt;

&lt;p&gt;For a 7B model at Q4, the same math gives ~4GB of weights. Add context and overhead, and an 8GB card (RTX 4060 Ti) handles it comfortably, which is why 7B models are the sweet spot for budget local setups.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step: Can You Run It?
&lt;/h2&gt;

&lt;p&gt;Let us apply the process to a real decision, the kind you will actually face. Imagine you want to run a 32B parameter model for local coding assistance, with a 16K context window, on a budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — compute the weights.&lt;/strong&gt; At Q4, 32B parameters means 32 × 4 / 8 = 16GB of weights. That alone rules out any 8GB card and most 12GB ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — budget the context.&lt;/strong&gt; A 16K context on a 32B model needs roughly 3-5GB of KV cache plus runtime overhead. Realistic total: 20-24GB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — compare against hardware.&lt;/strong&gt; An RTX 4090 (24GB) fits this at Q4 with a moderate context. An RTX 4060 Ti (8GB) does not even come close. The Mac Studio M5 Ultra (192GB) fits it trivially — but at 819 GB/s its generation speed is capped below what the 4090 achieves with its 1,008 GB/s, so your real choice is between the 24GB card for speed or the Mac for sheer capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — sanity-check the economics.&lt;/strong&gt; Before spending, run your real workload through the &lt;a href="https://notacalculator.com/calculator/hardware-requirements-calculator" rel="noopener noreferrer"&gt;LLM Hardware Requirements Calculator&lt;/a&gt; to confirm the model+context combo, and the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt; to confirm you actually use enough tokens to justify the hardware over cloud API calls. A 32B model you use twice a month is cheaper as cloud tokens than as a $1,500 GPU.&lt;/p&gt;

&lt;p&gt;That four-step flow — weights, context, hardware, economics — is the entire skill of answering "can I run it?" for any model you will ever see.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Tips for Buying or Repurposing a GPU
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check your real constraint before buying.&lt;/strong&gt; If you want 7B models, an 8GB card is enough; if you want 32B+, you are on the 24-32GB tier. Know the target before the budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch out for "weights only" marketing.&lt;/strong&gt; Advertised VRAM figures are often weights-only. Add 2-6GB for context and overhead when planning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantization is your friend, not a cheat.&lt;/strong&gt; Q4 (4-bit) is the quality/size sweet spot for most local use. Q3 fits bigger models but the quality loss shows on complex reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth determines speed, VRAM determines fit.&lt;/strong&gt; A 70B model on a huge unified-memory Mac runs slower than a 7B on a fast gaming card. The model you can fit is not the same as the model you can enjoy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test before you commit.&lt;/strong&gt; Download a GGUF file and measure your actual tokens-per-second with your real context before spending thousands on a bigger card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When NOT to buy a GPU:&lt;/strong&gt; if your workload is bursty and small, renting cloud tokens is cheaper — use the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt; to check before you spend.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations and Edge Cases
&lt;/h2&gt;

&lt;p&gt;The VRAM equation is a strong heuristic, not a precise oracle. Real usage varies with context length, batch size, KV-cache behavior, and the exact runtime (llama.cpp, MLX, vLLM all manage memory slightly differently). The "realistic total" column assumes a moderate context window; heavy RAG workloads with large contexts can push 5-10GB beyond the table. CPU offloading blurs the line further — a model can "run" while partially streaming through system RAM, at a steep speed penalty. And unified-memory systems (Apple Silicon, DGX Spark) have a different memory model where the entire system RAM is the pool, so the math changes entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: How much VRAM do I need for a 7B model?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;About 4GB of VRAM for the weights at Q4 quantization, plus 1-2GB for context and overhead. An 8GB card like the RTX 4060 Ti is the comfortable entry point for 7B models.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How much VRAM does a 70B model need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Roughly 35GB for Q4 weights, and 40-44GB with context and overhead. That exceeds a single RTX 5090's 32GB, so 70B at Q4 realistically needs two cards, a workstation GPU, or a high-VRAM unified-memory Mac.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I run a 70B model on 24GB VRAM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Only at Q3 or smaller quantization, or with CPU offloading. Q4 70B needs more than 24GB; you would sacrifice quality or speed to squeeze it in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is quantization and why does it matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Quantization stores model weights in fewer bits (8, 4, or 3 instead of 16), shrinking memory use roughly proportionally at a small quality cost. It is the reason local AI is feasible on consumer GPUs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does more VRAM always mean faster output?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;No. VRAM determines whether a model fits; bandwidth determines how fast it generates. A huge-model Mac can be slower than a small-model gaming card because of the bandwidth gap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is CPU offloading a good idea for a 70B model?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It lets a model run on insufficient VRAM by streaming layers through system RAM, but at a significant speed penalty. It is a stopgap, not a comfortable long-term setup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I run an LLM on a laptop with integrated graphics?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;You can, but only small models at low speed. A 1-3B model in Q4 fits in shared system memory and will crawl along at a few tokens per second. For anything interactive, a dedicated GPU or an Apple Silicon Mac with unified memory is the practical entry point.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens when a model does not fit in VRAM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Two things: either the runtime crashes with an out-of-memory error, or it silently falls back to CPU offloading and runs drastically slower. The fix is smaller quantization, a shorter context window, or a bigger card.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is a 32GB card enough for everything in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It handles the realistic consumer sweet spot: up to 32B models at Q4 and 70B at Q3, with room for context. It does not run 70B at Q4 or 120B-class models comfortably — for those you need dual-GPU, a workstation, or unified memory in the 96-192GB range.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does batch size change VRAM needs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Yes, dramatically. Processing multiple requests at once multiplies the KV-cache footprint per active sequence. Serving frameworks like vLLM handle batching efficiently, but a single-user setup with llama.cpp typically processes one request at a time, keeping cache usage lower.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/local-llm-vram-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>llm</category>
    </item>
    <item>
      <title>vLLM vs Ollama: Production Serving 2026</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:16:51 +0000</pubDate>
      <link>https://dev.to/apeder/vllm-vs-ollama-production-serving-2026-37kf</link>
      <guid>https://dev.to/apeder/vllm-vs-ollama-production-serving-2026-37kf</guid>
      <description>&lt;p&gt;&lt;em&gt;Compare vLLM and Ollama for LLM serving in 2026 — architecture, verified performance under concurrency, and a decision framework for choosing or combining them.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Tools for Two Very Different Jobs
&lt;/h2&gt;

&lt;p&gt;If you have run a large language model locally in the last two years, you have almost certainly touched Ollama. It is the tool that made local LLMs approachable: install it, pull a model, and run a chat in minutes. If you have served a model to hundreds of concurrent users in production, you have almost certainly touched vLLM. It is the workhorse behind many hosted inference platforms, built from the ground up for throughput at scale.&lt;/p&gt;

&lt;p&gt;The mistake most people make is treating them as interchangeable. They are not. They are built for different workloads, different concurrency profiles, and different priorities. This guide explains the architecture that makes them different, shows the verified performance gap under real conditions, and gives you a decision framework for choosing — or combining — them in 2026.&lt;/p&gt;

&lt;p&gt;The short version: at a single concurrent user, Ollama is simpler and can even be slightly faster. The moment you add concurrency — multiple users, parallel requests, a front-end app — vLLM pulls ahead, and the gap grows with the number of simultaneous requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Each Tool Actually Is
&lt;/h2&gt;

&lt;h3&gt;
  
  
  vLLM: a production serving engine
&lt;/h3&gt;

&lt;p&gt;vLLM, developed at UC Berkeley's Sky Computing Lab, is a high-throughput inference and serving library written in Python. It is not a "runner" that wraps a backend; it is a full serving stack with its own scheduler, memory manager, and batching engine. Its two signature ideas are &lt;strong&gt;PagedAttention&lt;/strong&gt; and &lt;strong&gt;continuous batching&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PagedAttention manages the KV cache — the memory that stores prior tokens during generation — the way an operating system manages pages of RAM. Instead of allocating one contiguous block per request, it stores tokens in fixed-size blocks that can point to non-contiguous memory. This eliminates the memory fragmentation that wastes up to 60-80% of KV cache in naive implementations, letting far more requests share the same GPU memory.&lt;/p&gt;

&lt;p&gt;Continuous batching goes further: instead of waiting for a whole batch of requests to finish before starting the next, it lets requests join and leave the batch as they complete. A request that finishes early frees its slot immediately, and a new request joins right away. This keeps the GPU saturated instead of idling while stragglers finish, and is the single biggest driver of vLLM's throughput advantage. vLLM supports 200+ model architectures and scales across multiple GPUs with tensor, pipeline, data, expert, and context parallelism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ollama: a local runner built on llama.cpp
&lt;/h3&gt;

&lt;p&gt;Ollama is a Go-based application that runs open-source models locally, built on the llama.cpp engine (with MLX support for Apple Silicon). Its entire design philosophy is simplicity: a clean CLI, a small API, and models distributed through its registry. It was built for the developer sitting at a terminal, running a model on one machine, chatting with it directly.&lt;/p&gt;

&lt;p&gt;That simplicity has a cost. Ollama is not designed for high concurrency. Its parallelism is capped by the &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; environment variable, which defaults to 4, and its scheduling model does not aggressively batch work the way vLLM does. For a single interactive user this rarely matters. For a service behind a load balancer it becomes the bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Verified Performance Gap
&lt;/h2&gt;

&lt;p&gt;The most reliable independent benchmark available compares Ollama and vLLM serving Llama 3.1 8B on an NVIDIA A100 40GB across a concurrency range from 1 to 256 simultaneous requests. The results are unambiguous.&lt;/p&gt;

&lt;p&gt;At a single request, the two tools are close. As concurrency rises, vLLM's continuous batching and PagedAttention take over, and the gap widens to nearly &lt;strong&gt;20x&lt;/strong&gt; at high concurrency: vLLM peaked around 793 tokens per second against Ollama's 41 tokens per second, with 99th-percentile latency of 80ms versus 673ms. Notably, Ollama remained behind even when its parallel limit was raised to 32.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Illustrative throughput vs concurrency for Llama 3.1 8B on A100 40GB. At single user they are close; under concurrency vLLM pulls ahead dramatically. Values approximate the Red Hat benchmark pattern.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The exact multiplier depends on the model, GPU, and workload — it is not a universal constant. A 2026 benchmark on an A100 80GB with Llama 3 8B found vLLM about &lt;strong&gt;2.3x faster at 8 concurrent users&lt;/strong&gt; (187 vs 82 tok/s) after Ollama actually edged ahead at a single user (45 vs 38 tok/s). Another test with a dual-GPU Qwen3 14B setup measured vLLM up to &lt;strong&gt;3.2x&lt;/strong&gt; ahead at 128 concurrent requests.&lt;/p&gt;

&lt;p&gt;Two things are consistent across every source: at one user the tools are comparable, and under real concurrency vLLM wins by a large margin. Anyone repeating the claim that "vLLM is 20-29x faster" should treat that as a high-concurrency figure on a specific workload, not a general rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;Ollama&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary use&lt;/td&gt;
&lt;td&gt;High-throughput production serving&lt;/td&gt;
&lt;td&gt;Local dev / single-user chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous batching&lt;/td&gt;
&lt;td&gt;Yes (default)&lt;/td&gt;
&lt;td&gt;No (capped parallelism)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PagedAttention&lt;/td&gt;
&lt;td&gt;Yes (core design)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI-compatible API&lt;/td&gt;
&lt;td&gt;Yes (+ Anthropic, gRPC)&lt;/td&gt;
&lt;td&gt;Yes (via /v1, no stateful)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-GPU / distributed&lt;/td&gt;
&lt;td&gt;Yes (tensor/pipeline/data/expert)&lt;/td&gt;
&lt;td&gt;Limited (single node)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantization support&lt;/td&gt;
&lt;td&gt;GGUF, GPTQ, AWQ, FP8, MXFP8&lt;/td&gt;
&lt;td&gt;GGUF (Q4_K_M, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency control&lt;/td&gt;
&lt;td&gt;Dynamic scheduler&lt;/td&gt;
&lt;td&gt;OLLAMA_NUM_PARALLEL (default 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Go (llama.cpp backend)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;em&gt;Approximate relative throughput advantage of vLLM over Ollama as concurrency grows (1x at single user to ~19x at high concurrency). Illustrative, based on the Red Hat benchmark.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Key Calculations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Token throughput
&lt;/h3&gt;

&lt;p&gt;Throughput is the number of tokens generated per second across all requests combined. It is the metric that matters for cost and capacity:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;h&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ro&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ug&lt;/span&gt;&lt;span class="mord mathnormal"&gt;h&lt;/span&gt;&lt;span class="mord mathnormal"&gt;p&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;im&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;k&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;g&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;er&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;vLLM maximizes throughput under load by keeping the GPU saturated via continuous batching. Ollama's throughput plateaus because its parallelism cap and less aggressive batching leave GPU capacity idle between requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency percentiles
&lt;/h3&gt;

&lt;p&gt;For interactive applications, the 99th-percentile latency (P99) matters more than the average, because it reflects the worst experience a user actually gets. Under concurrency, vLLM's P99 stayed at ~80ms while Ollama's degraded to ~673ms — the tail latency of Ollama under load is an order of magnitude worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost per token at scale
&lt;/h3&gt;

&lt;p&gt;The practical consequence is cost. At 100 concurrent users, the number of GPU-seconds required to serve a fixed request volume is far lower on vLLM because it packs more work into each GPU. This connects directly to the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt;: the throughput a framework extracts from your hardware determines how much self-hosting actually saves you.&lt;/p&gt;

&lt;h3&gt;
  
  
  The KV cache: where concurrency lives or dies
&lt;/h3&gt;

&lt;p&gt;The fundamental resource that limits concurrent inference is not raw compute — it is the &lt;strong&gt;KV cache&lt;/strong&gt; , the memory that holds the key-value representations of every token processed so far in a generation. Each active request holds its own KV cache for the duration of its generation, and with many concurrent requests the total can dwarf the model weights themselves.&lt;/p&gt;

&lt;p&gt;Naive serving allocates one contiguous block of KV memory per request up front. Because requests generate different numbers of tokens at different rates, these blocks are mostly wasted: a request that finishes early leaves its reserved memory idle, and fragmentation prevents the freed space from being reused efficiently. This is where vLLM's PagedAttention is decisive. By splitting the KV cache into fixed-size blocks that can be scattered across memory and shared between requests, it eliminates most of that fragmentation, letting a far larger number of requests coexist on the same GPU.&lt;/p&gt;

&lt;p&gt;The practical formula for sizing a server is therefore:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;V&lt;/span&gt;&lt;span class="mord mathnormal"&gt;R&lt;/span&gt;&lt;span class="mord mathnormal"&gt;A&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;M&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;l&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;W&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;V&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;p&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;e&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;r&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;r&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;q&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;es&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Where 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;W&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the model weights, 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;K&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;V&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;p&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;e&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;r&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;r&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;q&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;u&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;es&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the KV cache per active request (proportional to context length and model size), and 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 is the concurrency. The ability to pack more requests per gigabyte of KV memory is precisely what lets vLLM serve higher concurrency on the same hardware — and why an Ollama default cap of four parallel requests underutilizes a data-center GPU that could hold dozens.&lt;/p&gt;

&lt;p&gt;This is also why context length matters so much. A model serving very long contexts consumes KV memory far faster, so the trade-off between concurrency and context length is real. A server tuned for 200K-token contexts supports far fewer concurrent requests than one serving short prompts, regardless of framework. Understanding this relationship — rather than treating throughput as a single number — is the difference between a configuration that serves your users and one that OOMs at the worst moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Ollama
&lt;/h2&gt;

&lt;p&gt;Ollama is the right tool when simplicity and iteration speed matter more than concurrency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local development&lt;/strong&gt; — prototyping a prompt or testing a model on your laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-user tooling&lt;/strong&gt; — a CLI assistant, a personal notebook, an experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model exploration&lt;/strong&gt; — pulling and comparing models from its registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Education and learning&lt;/strong&gt; — the lowest-friction way to get started with local LLMs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-device / edge scenarios&lt;/strong&gt; — a small model on a workstation or Apple Silicon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workload is one person talking to one model on one machine, Ollama is not just easier — it can be slightly faster, since it avoids vLLM's serving overhead. Its cold start is also faster (about 3 seconds vs 9 seconds in one 2026 test), which matters for interactive-first use.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use vLLM
&lt;/h2&gt;

&lt;p&gt;vLLM is the right tool when you are serving a model to other people or machines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production API&lt;/strong&gt; — multiple concurrent users hitting a model behind a load balancer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents and automation&lt;/strong&gt; — many parallel agents making inference calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High request volume&lt;/strong&gt; — throughput and P99 latency are business requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-GPU models&lt;/strong&gt; — models too large for one GPU need vLLM's distributed inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantization variety&lt;/strong&gt; — AWQ, GPTQ, and FP8 support beyond GGUF.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is operational complexity. vLLM requires more setup: it is Python-based, needs a scheduling and serving configuration, and rewards someone who understands GPU memory and batching. That complexity buys you the ability to serve dozens or hundreds of users from one node.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assuming vLLM is always faster.&lt;/strong&gt; At a single concurrent user it is comparable to, sometimes slower than, Ollama. The advantage only appears under concurrency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quoting a single throughput number without concurrency context.&lt;/strong&gt;"vLLM is 20x faster" is meaningless without the concurrency and workload it was measured at. Always ask: at what concurrency, on what model and GPU?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using Ollama as a production server without tuning.&lt;/strong&gt; Its default parallel limit of 4 becomes a hard ceiling for any real service. Raising &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; helps only marginally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring tail latency.&lt;/strong&gt; Average latency can look fine while P99 is unusable. Under concurrency, measure percentiles, not just averages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating them as mutually exclusive.&lt;/strong&gt; Many teams use Ollama for development and vLLM for the same model in production — the same GGUF or model files feed both.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  A Practical Decision Framework
&lt;/h2&gt;

&lt;p&gt;Ask three questions in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How many concurrent users?&lt;/strong&gt; One, or a handful on the same machine → Ollama. Tens or hundreds → vLLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is this a service with an SLA?&lt;/strong&gt; If yes, you need vLLM's P99 control and throughput. If it is an interactive single-user tool, Ollama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you need multi-GPU or specific quantizations?&lt;/strong&gt; vLLM for distributed inference and AWQ/FP8; Ollama's GGUF simplicity otherwise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In many real 2026 deployments the answer is a hybrid: develop and prototype with Ollama, then serve the same model to production traffic with vLLM. The model files are largely interchangeable, so you get the best of both — Ollama's low-friction iteration and vLLM's production throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hardware Considerations (2026)
&lt;/h2&gt;

&lt;p&gt;Production concurrency was benchmarked on NVIDIA data-center GPUs — the A100 40GB and 80GB — which remain the reference for multi-user serving. Consumer cards like the RTX 5090 (32GB GDDR7) handle single-user and light concurrency well, but for sustained high-concurrency throughput, a data-center GPU with more memory bandwidth and larger VRAM, or multiple GPUs behind vLLM's distributed inference, is the practical choice.&lt;/p&gt;

&lt;p&gt;There is no universal "minimum GPU" — it depends on model size, quantization, and context length. The reliable approach is to benchmark your specific model and concurrency target, exactly as the sources behind this guide did. Use the &lt;a href="https://notacalculator.com/calculator/hardware-requirements-calculator" rel="noopener noreferrer"&gt;Hardware Requirements Calculator&lt;/a&gt; to estimate VRAM for your model and then measure real throughput on your target hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quantization as the multiplier
&lt;/h3&gt;

&lt;p&gt;Quantization is the cheapest way to shift the trade-off in your favor, and it is where the two tools differ in flexibility. Ollama's strength is the GGUF format and its Q4_K_M and similar quantizations from the llama.cpp ecosystem — enough to run a model on consumer hardware that would otherwise not fit. vLLM supports GGUF too, but adds AWQ, GPTQ, and FP8/MXFP8 quantization that preserve accuracy better at scale and integrate with its batching scheduler.&lt;/p&gt;

&lt;p&gt;A quantized model is not only smaller in VRAM; it directly reduces the KV cache per token and often improves throughput, because the GPU processes fewer bytes per operation. The catch is accuracy: more aggressive quantization trades output quality for speed and memory. In production, teams typically benchmark a Q4 versus a Q8 or FP8 version of the same model on their real workload to find the smallest quantization that still passes their quality bar. The token counter and context tools on this site help you estimate the memory and throughput impact of that choice before you commit hardware to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is vLLM always faster than Ollama?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;No. At a single concurrent user the two are comparable, and Ollama can even edge ahead due to lower overhead. vLLM's advantage appears and grows with concurrency: on an A100 40GB running Llama 3.1 8B, vLLM reached roughly 793 tok/s vs Ollama's 41 tok/s at high concurrency, but at one user they were close.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can Ollama serve a production API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Technically yes, but it is not designed for it. Its parallelism is capped (OLLAMA_NUM_PARALLEL, default 4) and it lacks vLLM's continuous batching, so throughput and tail latency degrade under real concurrency. For a single user or light internal tool it is fine; for a public service use vLLM.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do Ollama and vLLM use the same model files?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Largely yes. Ollama uses GGUF (from llama.cpp), and vLLM also supports GGUF among many formats (GPTQ, AWQ, FP8, MXFP8). This makes it practical to prototype with Ollama and serve the same model with vLLM in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is continuous batching and why does it matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It lets requests join and leave a GPU batch as they complete, instead of waiting for the whole batch to finish. This keeps the GPU saturated and is the main reason vLLM's throughput scales with concurrency while Ollama's plateaus.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is the 20-29x claim about vLLM accurate?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It is a high-concurrency figure on a specific workload, not a general rule. A widely cited 23x number was measured against naive static batching, not Ollama. A Red Hat benchmark put vLLM around 19x ahead of Ollama at high concurrency on an A100. Always ask about concurrency and hardware before trusting a multiplier.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Which is better for a single developer building an app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;For development and iteration, Ollama is the low-friction choice. When you deploy that app so many users hit the model concurrently, move the serving layer to vLLM. Many teams use both: Ollama to build, vLLM to serve.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need a data-center GPU for vLLM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Not for small models or light concurrency — a consumer card like the RTX 5090 works. For sustained high-concurrency serving, a data-center GPU (like the A100 used in benchmarks) or multiple GPUs behind vLLM's distributed inference is more practical. Benchmark your model and concurrency target to decide.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/vllm-vs-ollama-production-serving" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>guide</category>
      <category>ai</category>
      <category>llm</category>
      <category>reviews</category>
    </item>
    <item>
      <title>Local vs Hosted LLMs: The Decision Framework</title>
      <dc:creator>Adolfo Pedernera</dc:creator>
      <pubDate>Thu, 13 Aug 2026 19:03:21 +0000</pubDate>
      <link>https://dev.to/apeder/local-vs-hosted-llms-the-decision-framework-56p</link>
      <guid>https://dev.to/apeder/local-vs-hosted-llms-the-decision-framework-56p</guid>
      <description>&lt;p&gt;&lt;strong&gt;A comprehensive framework for deciding between local LLMs and cloud APIs. Covers cost, privacy, latency, control, and the hybrid approach.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cathedral and the Bazaar, Revisited
&lt;/h2&gt;

&lt;p&gt;In 1997, Eric Raymond published an essay that framed a fundamental tension in software: the cathedral (centralized, carefully crafted, top-down) versus the bazaar (distributed, chaotic, bottom-up). Three decades later, that tension defines one of the most consequential decisions in modern software architecture: should you run large language models on your own hardware, or send your data to someone else's cloud?&lt;/p&gt;

&lt;p&gt;The question is not abstract. In 2026, a startup shipping AI-powered features faces it directly. So does a hospital wanting to analyze patient records with LLMs. So does a solo developer building a coding agent. The answer shapes cost structure, latency profile, privacy posture, and strategic flexibility for years to come.&lt;/p&gt;

&lt;p&gt;This guide provides a framework for that decision — not a one-size-fits-all answer, but a way to think about the tradeoffs with clarity. We will examine cost (using the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt;), performance, privacy, latency, vendor risk, and the increasingly popular hybrid architectures that combine both approaches.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Brief History: From Mainframes to APIs
&lt;/h2&gt;

&lt;p&gt;Centralized computing is not new. In the 1960s, organizations rented time on mainframes they could not afford to own. The personal computer revolution of the 1980s shifted power to the edge — every desktop became a computing platform. The cloud era of the 2000s recentralized, offering unlimited scale without capital expenditure. And now, in the 2020s, generative AI has created a new pendulum swing: the most capable models live in the cloud, but open-weight alternatives are powerful enough to run locally.&lt;/p&gt;

&lt;p&gt;The economics of each era followed the same pattern: centralized solutions win on convenience and time-to-market; distributed solutions win on control, long-term cost, and independence. LLMs are no different.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cost Dimension
&lt;/h2&gt;

&lt;p&gt;Cost is where most analyses start, and often where they end — incorrectly. The naive comparison is seductive: a cloud API charges $2-30 per million tokens, while a GPU costs a fixed amount amortized over years. But this framing misses the full picture. Use the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt; to find your specific crossover point, and the &lt;a href="https://notacalculator.com/calculator/hardware-requirements-calculator" rel="noopener noreferrer"&gt;LLM Hardware Requirements Calculator&lt;/a&gt; to determine which hardware can actually run your target model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beyond Per-Token Pricing
&lt;/h3&gt;

&lt;p&gt;Cloud API pricing in 2026 spans two orders of magnitude. GPT-5.6 Luna costs $0.20 per million input tokens; Claude Fable 5 costs $10.00. DeepSeek V4 Flash undercuts everyone at $0.14. The right comparison depends entirely on which model tier your use case requires.&lt;/p&gt;

&lt;p&gt;Self-hosting costs include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware amortization&lt;/strong&gt; : A $3,500 RTX 5090 (current street price, well above $1,999 MSRP) amortized over 36 months is ~$97/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electricity&lt;/strong&gt; : 575W × 16hr/day × 30 days × $0.17/kWh = ~$47/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering labor&lt;/strong&gt; : 2-20 hours/month depending on scale, at $75-150/hour fully loaded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model updates&lt;/strong&gt; : Manual evaluation and redeployment (4-6 cycles per year)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opportunity cost&lt;/strong&gt; : Time spent on infrastructure instead of product development&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Break-Even Calculation
&lt;/h3&gt;

&lt;p&gt;Using the &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt;, you can find your specific crossover point. As a rule of thumb with August 2026 pricing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Against Claude Sonnet 5 ($2/$10 intro pricing): ~135M tokens/month with 2× RTX 5090&lt;/li&gt;
&lt;li&gt;Against GPT-5.6 Sol ($5/$30): ~40M tokens/month with single RTX 5090&lt;/li&gt;
&lt;li&gt;Against DeepSeek V4 Flash ($0.14/$0.28): essentially unreachable for most teams&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Utilization Trap
&lt;/h3&gt;

&lt;p&gt;A GPU at 80% utilization has roughly half the effective per-token cost of the same GPU at 40%. At 10% utilization — common for bursty, unpredictable workloads — the cost is 8x higher than at 80%. This is why self-hosting rewards steady, predictable demand and penalizes sporadic usage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Privacy Dimension
&lt;/h2&gt;

&lt;p&gt;Privacy is the non-cost reason that often drives the decision. When you send data to a cloud API, it traverses the internet, touches another organization's infrastructure, and may be logged, cached, or used for model training depending on the provider's terms. Even with strong encryption in transit and at rest, you are entrusting sensitive information to a third party's operational security.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regulatory Landscape
&lt;/h3&gt;

&lt;p&gt;In 2026, data protection regulations continue to tighten globally. GDPR in Europe, HIPAA in US healthcare, and emerging frameworks in Asia impose strict requirements on where data can be processed and stored. For organizations in regulated industries, local processing is often not a preference but a legal requirement. Cross-border data transfers face increasing scrutiny, and the Schrems II ruling has made US cloud providers' compliance more complex for European organizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Trust Spectrum
&lt;/h3&gt;

&lt;p&gt;Different providers offer different privacy postures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard cloud APIs&lt;/strong&gt; : Data may be used for training (opt-out available), retained for 30 days for abuse monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise tiers&lt;/strong&gt; : Data not used for training, shorter retention, available in select regions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local deployment&lt;/strong&gt; : Data never leaves your infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For use cases involving trade secrets, patient data, classified information, or simply user conversations you would not want exposed, local deployment eliminates an entire category of risk. The question is not whether cloud providers are trustworthy — it is whether you want to depend on their trustworthiness for your most sensitive data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Latency Dimension
&lt;/h2&gt;

&lt;p&gt;Cloud APIs add network round-trip time — typically 50-200ms before the first token, depending on your location and the provider's infrastructure. For interactive use cases where a human waits on the response, this matters. A voice assistant that takes 200ms to begin speaking feels sluggish; a coding suggestion that appears after a perceptible delay disrupts flow.&lt;/p&gt;

&lt;p&gt;Local inference on an RTX 5090 produces the first token in 50-100ms with no network dependency. For applications like voice assistants, real-time coding suggestions, or industrial control systems, this difference is decisive. The variance is also lower — you are not competing with other users for GPU time or subject to provider-side queuing.&lt;/p&gt;

&lt;p&gt;However, for batch processing (summarization, classification, overnight report generation), latency is irrelevant — cloud APIs work fine and scale automatically. The latency dimension only matters when a human or real-time system waits on the response.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Control Dimension
&lt;/h2&gt;

&lt;p&gt;Cloud APIs introduce dependency on an external vendor. This manifests in several ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing changes&lt;/strong&gt; : API prices have fallen roughly 80% over 2025-2026, benefiting users. But providers can raise prices, and switching costs are real. Teams that built products around GPT-4.1's $2/$8 pricing faced margin pressure when competitors launched cheaper alternatives. Building on local infrastructure eliminates this variability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model deprecation&lt;/strong&gt; : When a provider retires a model, you must migrate. Cloud providers have retired capable models with limited notice, forcing rushed migrations. Local deployment means you control when and whether to upgrade. You can run a model indefinitely, even if the provider moves on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits and availability&lt;/strong&gt; : Cloud APIs enforce rate limits that require engineering workarounds (retry logic, request queuing). Outages are outside your control — when a major provider goes down, your product goes down with it. Local deployment provides predictable availability limited only by your own infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature availability&lt;/strong&gt; : New capabilities (tool use, vision, longer context) arrive on cloud APIs first. Local deployment lags by weeks to months depending on the open-source ecosystem. If your product requires the absolute latest capabilities, cloud APIs provide earlier access.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hybrid Architecture
&lt;/h2&gt;

&lt;p&gt;For many teams in 2026, the optimal answer is not "local OR cloud" but "local AND cloud" — a deliberate hybrid architecture that routes each request to the most appropriate backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing Logic
&lt;/h3&gt;

&lt;p&gt;A well-designed hybrid system routes based on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request Type&lt;/th&gt;
&lt;th&gt;Recommended Backend&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-volume, simple tasks&lt;/td&gt;
&lt;td&gt;Local model&lt;/td&gt;
&lt;td&gt;Free after hardware amortization; keeps GPUs utilized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy-sensitive data&lt;/td&gt;
&lt;td&gt;Local model&lt;/td&gt;
&lt;td&gt;Data never leaves infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency-critical interactions&lt;/td&gt;
&lt;td&gt;Local model&lt;/td&gt;
&lt;td&gt;No network round-trip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex reasoning requiring frontier capabilities&lt;/td&gt;
&lt;td&gt;Cloud API (GPT-5.6 Sol, Claude Fable 5)&lt;/td&gt;
&lt;td&gt;Capabilities not available locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burst overflow beyond local capacity&lt;/td&gt;
&lt;td&gt;Cloud API&lt;/td&gt;
&lt;td&gt;Scale without provisioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New model evaluation&lt;/td&gt;
&lt;td&gt;Cloud API&lt;/td&gt;
&lt;td&gt;Access before open weights available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;The simplest hybrid implementation uses a proxy layer that inspects each request and routes accordingly. Local inference typically runs on vLLM or Ollama depending on concurrency needs — vLLM delivers 20-29x higher throughput under concurrent load thanks to continuous batching and PagedAttention, while Ollama prioritizes simplicity for single-user scenarios. More sophisticated implementations use a local model for initial classification, then escalate to cloud only when confidence is low.&lt;/p&gt;

&lt;p&gt;The key metric to track is GPU utilization. Move steady, high-volume traffic onto local models and the per-token figures start working in your favor; reserve cloud APIs for spikes and frontier-model calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Framework: A Checklist
&lt;/h2&gt;

&lt;p&gt;When evaluating local versus hosted LLMs for your specific use case, work through these questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume and utilization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;◻️What is your projected monthly token volume?&lt;/li&gt;
&lt;li&gt;◻️Is usage steady or bursty?&lt;/li&gt;
&lt;li&gt;◻️Would local GPUs run at &amp;gt;40% average utilization?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quality requirements&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;◻️Does your use case require frontier model capabilities (GPT-5.6 Sol, Claude Fable 5)?&lt;/li&gt;
&lt;li&gt;◻️Can a 7B-30B open-weight model handle &amp;gt;80% of your requests?&lt;/li&gt;
&lt;li&gt;◻️Have you benchmarked open-weight models on your actual workload?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Privacy and compliance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;◻️Does your data fall under GDPR, HIPAA, or similar regulations?&lt;/li&gt;
&lt;li&gt;◻️Are you willing to send data to a third-party API?&lt;/li&gt;
&lt;li&gt;◻️Do you need data residency in specific geographic regions?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Latency and reliability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;◻️Is sub-100ms time-to-first-token required?&lt;/li&gt;
&lt;li&gt;◻️Can you tolerate cloud API outages?&lt;/li&gt;
&lt;li&gt;◻️Do you need guaranteed availability SLAs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Engineering capacity&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;◻️Does your team have GPU infrastructure expertise?&lt;/li&gt;
&lt;li&gt;◻️Can you budget 5-20 hours/month for ops and model updates?&lt;/li&gt;
&lt;li&gt;◻️Do you have hardware procurement and maintenance capacity?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you answer "yes" to most volume, privacy, and latency questions, and "no" to frontier-quality requirements, local deployment deserves serious consideration. If you need frontier capabilities, have bursty usage, or lack infrastructure expertise, cloud APIs remain the rational starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Recommendations by Team Type
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Solo Developers and Small Teams (fewer than 5 engineers)
&lt;/h3&gt;

&lt;p&gt;Start with cloud APIs. The engineering overhead of self-hosting is a distraction from product development. Use the cheapest model tier that handles each task (DeepSeek V4 Flash for classification, Claude Sonnet 5 for complex work). Revisit when monthly API spend exceeds $500.&lt;/p&gt;

&lt;h3&gt;
  
  
  Growth-Stage Startups (5-20 engineers)
&lt;/h3&gt;

&lt;p&gt;Run a hybrid stack. Self-host a 7B-14B model for high-volume, simple tasks (classification, extraction, routing). Route complex requests to frontier cloud APIs. This typically reduces cloud spend by 40-60% while maintaining capability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise and Regulated Industries
&lt;/h3&gt;

&lt;p&gt;Deploy local-first for any workload touching sensitive data. Use cloud APIs only for non-sensitive workloads and frontier capabilities. Budget for dedicated MLOps support (0.2-0.5 FTE per model in production).&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-Native Products
&lt;/h3&gt;

&lt;p&gt;If your product's core value is AI inference, self-hosting at scale becomes a competitive advantage. At 500M+ tokens/month on premium models, local deployment can save $50K+ annually versus cloud APIs — savings that drop directly to your bottom line. Some AI-native companies have made local inference a core part of their infrastructure strategy, offering it as a privacy guarantee to customers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Future: Convergence or Divergence?
&lt;/h2&gt;

&lt;p&gt;The local-cloud tension will not resolve soon. Cloud models continue to improve faster than open-weight alternatives, but the gap is narrowing. Models like Qwen 3.6 and Gemma 4 deliver frontier-adjacent quality on consumer hardware. Inference tooling (vLLM, llama.cpp, MLX) matures monthly, making local deployment increasingly accessible.&lt;/p&gt;

&lt;p&gt;What is converging is the tooling: deploying a local model in 2026 is dramatically simpler than in 2024. One-command setup, automatic quantization, and better GPU utilization have lowered the barrier significantly. What remains divergent is the capability ceiling: the most capable models (Claude Fable 5, GPT-5.6 Sol) are available only via API, and the gap between frontier and open-weight models persists for complex reasoning tasks.&lt;/p&gt;

&lt;p&gt;The hardware trajectory also favors local deployment. Each generation of consumer GPUs brings more VRAM and bandwidth — the RTX 5090's 32GB fits models that required enterprise hardware two years ago. Apple's M5 Ultra with 192GB unified memory can run 120B+ parameter models natively.&lt;/p&gt;

&lt;p&gt;The pragmatic path for most teams is to start with cloud, instrument actual usage, and migrate workloads to local hardware as volume grows and open-weight models improve. The &lt;a href="https://notacalculator.com/calculator/local-llm-break-even-calculator" rel="noopener noreferrer"&gt;Local LLM Break-Even Calculator&lt;/a&gt; tells you when that crossover point arrives for your specific numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I run an LLM locally or use a cloud API in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;It depends on your volume, quality requirements, and privacy needs. For most individuals and small teams, cloud APIs are cheaper and simpler. Self-hosting wins at high, steady volume (typically 50M+ tokens/month on premium models), when privacy regulations require local processing, or when sub-100ms latency is critical. A hybrid approach — local for baseline load, cloud for frontier capabilities and overflow — is optimal for many teams.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the cheapest way to use LLMs in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;For cloud APIs, DeepSeek V4 Flash at $0.14/$0.28 per million tokens is the cheapest frontier-class model. For local deployment, a used RTX 4090 ($1,600-2,000) running a 7B model at Q4 quantization delivers the lowest per-token cost. But remember: at low utilization, even 'free' local inference costs more than cloud APIs when you factor in hardware amortization and engineering time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How much does it cost to self-host an LLM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Hardware: $1,800-5,000 for a consumer GPU (RTX 4090/5090) or $3,000-8,000 for Mac Studio. Monthly operating costs: $30-100 electricity, $150-3,000 engineering labor depending on scale. At full GPU utilization, raw inference costs $0.05-0.10 per million tokens. At realistic 10-30% utilization, effective cost is $0.50-5.00 per million tokens.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is local AI more private than cloud APIs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Yes. Local deployment means data never leaves your infrastructure. Cloud APIs transit data over the internet and may log, cache, or (depending on terms) use it for training. For regulated data (healthcare, finance, classified) or sensitive business information, local processing eliminates an entire category of privacy and compliance risk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What GPU do I need to run LLMs locally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;For 7B-14B models: RTX 4090 (24GB) or RTX 5090 (32GB) at $1,600-5,000. For 70B models: 2× RTX 5090 or Mac Studio M5 Max/Ultra with 128-192GB unified memory ($3,000-8,000). The RTX 5090 fits 70B only at Q3 quantization or smaller; for Q4 70B, partial CPU offloading is required, dropping throughput significantly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do vLLM and Ollama compare for production use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Ollama (built on llama.cpp) is designed for simplicity — single-user, low-concurrency workloads. vLLM is optimized for production serving with continuous batching and PagedAttention, delivering 20-29x higher throughput under concurrent load. For personal use, choose Ollama. For production with multiple users, vLLM or similar production frameworks are the appropriate choice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is prompt caching and how does it affect cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Prompt caching discounts repeated input tokens. OpenAI applies it automatically (~90% off cached input for prompts over ~1K tokens). Anthropic requires explicit cache breakpoints and charges ~25% extra on cache writes. For chatbots with repeated system prompts, caching reduces input costs by 30-50%.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I estimate my monthly token usage?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: &lt;em&gt;Log token counts from API responses with a session ID and timestamp. Alternatively, use the Token Counter Calculator with your typical prompts to estimate average tokens per request, then multiply by request volume. Most workloads follow a 3:1 input-to-output ratio; coding assistants skew 8:1 input-heavy.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;notAcalculator&lt;/strong&gt; provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://notacalculator.com/guides/local-vs-hosted-llms-guide" rel="noopener noreferrer"&gt;Original guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>guide</category>
      <category>ai</category>
      <category>llm</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
