<?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: Zhang Enquan</title>
    <description>The latest articles on DEV Community by Zhang Enquan (@devenquan).</description>
    <link>https://dev.to/devenquan</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%2F4106337%2F2ae76bea-33aa-47de-a025-1235317f1001.png</url>
      <title>DEV Community: Zhang Enquan</title>
      <link>https://dev.to/devenquan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devenquan"/>
    <language>en</language>
    <item>
      <title>I Tokenized 10,000 Real Prompts: The "4 Characters 1 Token" Rule Is Off by More Than 20% on 4 Out of 10 of Them</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:21:06 +0000</pubDate>
      <link>https://dev.to/devenquan/i-tokenized-10000-real-prompts-the-4-characters-1-token-rule-is-off-by-more-than-20-on-4-out-3jc1</link>
      <guid>https://dev.to/devenquan/i-tokenized-10000-real-prompts-the-4-characters-1-token-rule-is-off-by-more-than-20-on-4-out-3jc1</guid>
      <description>&lt;p&gt;For three months I priced every LLM feature with one number: &lt;strong&gt;4 characters ≈ 1 token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is everywhere. It is in blog posts, in pricing calculators, in the back of my head when somebody asks "roughly how much will this cost". It feels safe because it is conservative: it rounds &lt;em&gt;up&lt;/em&gt; the token count, so your estimate lands above the real bill.&lt;/p&gt;

&lt;p&gt;Last week I stopped trusting it and measured it. 10,000 real samples, real tokenizer, no synthetic text.&lt;/p&gt;

&lt;p&gt;The rule is bad per prompt. It is fine for the monthly total. Knowing which of those two you are doing turns out to be the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured it
&lt;/h2&gt;

&lt;p&gt;I did not want synthetic text, because synthetic text is where benchmarks go to lie. The corpus is 521 real posts pulled from the dev.to public API, across &lt;code&gt;ai&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;programming&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt; and &lt;code&gt;opensource&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each post was stripped of front matter, then split into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prose&lt;/strong&gt; — paragraphs with code fences removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;code&lt;/strong&gt; — the contents of fenced blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;structured&lt;/strong&gt; — blocks that are JSON-ish or log-shaped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gave a pool I sampled 10,000 from: &lt;strong&gt;7,055 prose, 2,669 code, 275 structured&lt;/strong&gt;. Every sample was encoded with &lt;code&gt;cl100k_base&lt;/code&gt; through &lt;code&gt;tiktoken&lt;/code&gt;, one at a time, and I compared the real count against &lt;code&gt;len(text) / 4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two honest caveats before the numbers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This is &lt;strong&gt;English technical writing&lt;/strong&gt;. I got exactly one CJK sample in the whole corpus, so I make no claim about Chinese, Japanese, Korean or emoji-heavy text. If your traffic is multilingual, treat every number here as a lower bound on how wrong the rule is.&lt;/li&gt;
&lt;li&gt;The mix is heavy on prose because that is what dev.to posts are. Do not read the "all" column as "the internet".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the rule does to a single prompt
&lt;/h2&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%2Fuh99p2fiuy90s4423clu.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%2Fuh99p2fiuy90s4423clu.png" alt="Distribution of chars/4 estimation error on 10,000 real samples" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median error&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+11.7%&lt;/strong&gt; (the rule over-counts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median absolute error&lt;/td&gt;
&lt;td&gt;16.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Samples off by more than 20%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;of those, over-counted&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;of those, under-counted&lt;/td&gt;
&lt;td&gt;7.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5th percentile&lt;/td&gt;
&lt;td&gt;−23.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95th percentile&lt;/td&gt;
&lt;td&gt;+46.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So on a single prompt, the rule is a coin flip that is usually wrong by more than a fifth. If you have ever sized a context window with &lt;code&gt;chars / 4&lt;/code&gt; and wondered why the request still got truncated, that 7.2% under-count is why. The rule is mostly conservative, but "mostly" is not "always", and the failure mode is the expensive one: you packed too much and the call failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not expect: the monthly bill was basically right
&lt;/h2&gt;

&lt;p&gt;Here is the aggregate over all 10,000 samples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2,715,542 characters&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;646,092 real tokens&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;4.203 characters per token&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run &lt;code&gt;chars / 4&lt;/code&gt; over the whole pile and you get 678,885 tokens against a true 646,092. That is &lt;strong&gt;+5.08%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Read those two sections together, because they look like they contradict each other. They do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per prompt:&lt;/strong&gt; 40% of samples are off by more than 20%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In aggregate:&lt;/strong&gt; the batch is off by 5%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The errors cancel. Short samples and long samples, prose and code, they all pull the average toward 4.2 from different directions. So the rule is not "wrong" in the way people say it is wrong. It is &lt;strong&gt;unusable per call and acceptable per month&lt;/strong&gt;. If you are budgeting a feature, &lt;code&gt;chars / 4&lt;/code&gt; will get you a number that is about 5% high, which is fine. If you are deciding whether one specific prompt fits in a 128k window, it will get you a number that is wrong one time in three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short prompts are where it actually breaks
&lt;/h2&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%2Fic9n9pys8qp8fmh3guid.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%2Fic9n9pys8qp8fmh3guid.png" alt="Chars per token by sample length and content type" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I bucketed by length and this is the finding I now care about most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample length&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Median chars/token&lt;/th&gt;
&lt;th&gt;Median abs error&lt;/th&gt;
&lt;th&gt;Off by &amp;gt;20%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–200 chars&lt;/td&gt;
&lt;td&gt;5,455&lt;/td&gt;
&lt;td&gt;4.500&lt;/td&gt;
&lt;td&gt;17.5%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200–500 chars&lt;/td&gt;
&lt;td&gt;3,448&lt;/td&gt;
&lt;td&gt;4.537&lt;/td&gt;
&lt;td&gt;16.6%&lt;/td&gt;
&lt;td&gt;41.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500–1500 chars&lt;/td&gt;
&lt;td&gt;951&lt;/td&gt;
&lt;td&gt;4.186&lt;/td&gt;
&lt;td&gt;10.6%&lt;/td&gt;
&lt;td&gt;24.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1500–4000 chars&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;4.018&lt;/td&gt;
&lt;td&gt;4.8%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shorter the text, the worse the rule. Below 200 characters — which is most chat prompts, most system messages, most tool descriptions — it is off by more than 20% on 43.6% of samples. Above 1,500 characters it is nearly fine.&lt;/p&gt;

&lt;p&gt;That is the opposite of what intuition suggests. You would think a tiny string is easy to estimate. It is not: a single rare word, an unusual identifier or one emoji can swallow a dozen characters into three tokens, and there is no averaging to save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content type moves the constant
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Median chars/token&lt;/th&gt;
&lt;th&gt;Weighted chars/token&lt;/th&gt;
&lt;th&gt;Median error with /4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prose&lt;/td&gt;
&lt;td&gt;7,055&lt;/td&gt;
&lt;td&gt;4.577&lt;/td&gt;
&lt;td&gt;4.375&lt;/td&gt;
&lt;td&gt;+14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;2,669&lt;/td&gt;
&lt;td&gt;4.156&lt;/td&gt;
&lt;td&gt;4.010&lt;/td&gt;
&lt;td&gt;+3.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured (JSON, logs)&lt;/td&gt;
&lt;td&gt;275&lt;/td&gt;
&lt;td&gt;3.778&lt;/td&gt;
&lt;td&gt;3.877&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−5.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Structured is the one that should worry you. With &lt;code&gt;chars / 4&lt;/code&gt; it &lt;strong&gt;under-counts&lt;/strong&gt;: median error −5.6%, and &lt;strong&gt;21.1% of structured samples were under-counted by more than 20%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which means: if your app emits JSON and you set a hard token cap by dividing characters by four, you are setting that cap too low roughly a fifth of the time. You will reject responses you could have afford. That is the mirror image of the truncation bug, and it is the one nobody writes about.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which number should you use?
&lt;/h2&gt;

&lt;p&gt;I tried a range of constants against the same 10,000 samples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Median abs error&lt;/th&gt;
&lt;th&gt;Median bias&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;16.3%&lt;/td&gt;
&lt;td&gt;+11.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.25&lt;/td&gt;
&lt;td&gt;13.5%&lt;/td&gt;
&lt;td&gt;+5.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4.47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.03%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.5&lt;/td&gt;
&lt;td&gt;11.9%&lt;/td&gt;
&lt;td&gt;−0.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.75&lt;/td&gt;
&lt;td&gt;12.1%&lt;/td&gt;
&lt;td&gt;−5.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;4.47 is the unbiased one — it splits the difference almost perfectly. 4.5 is indistinguishable in practice and easier to remember. But notice the ceiling: even the best constant still has a &lt;strong&gt;median absolute error of 11.9%&lt;/strong&gt;. No single number fixes this. The distribution is too wide for a constant to win.&lt;/p&gt;

&lt;p&gt;My rules now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Budget with 4.5, bill with a real tokenizer.&lt;/strong&gt; 4.5 for the spreadsheet, actual &lt;code&gt;encode()&lt;/code&gt; before anything that costs money or hits a limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never set a hard cap with &lt;code&gt;chars / 4&lt;/code&gt; on structured output.&lt;/strong&gt; That is the one direction where the rule fails cheap instead of safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything under 200 characters, do not estimate.&lt;/strong&gt; Measure it once, cache the number. These are the samples where the rule is worst and they are also the ones you reuse a million times, so one measurement pays for itself immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Counting it for real
&lt;/h2&gt;

&lt;p&gt;If you want to see where a tokenizer actually splits your text — which is the only way to build intuition for why &lt;code&gt;résumé&lt;/code&gt; and &lt;code&gt;resume&lt;/code&gt; are not the same price — I built a small counter that shows the split boundaries per word: &lt;a href="https://aisubtools.xyz/llm-token-counter/" rel="noopener noreferrer"&gt;LLM Token Counter&lt;/a&gt;. It runs in the browser, no upload.&lt;/p&gt;

&lt;p&gt;If you would rather run it yourself in Python or Node, the walkthrough is here: &lt;a href="https://aisubtools.xyz/how-to-count-gpt-tokens/" rel="noopener noreferrer"&gt;How to count GPT tokens&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And for the case where characters are the unit you actually care about: &lt;a href="https://aisubtools.xyz/word-character-counter-tool/" rel="noopener noreferrer"&gt;Word &amp;amp; character counter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One question
&lt;/h2&gt;

&lt;p&gt;I can tell you about English technical prose. I cannot tell you about yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you have measured this on your own traffic — especially non-English, chat logs, or agent traces — what constant did you land on?&lt;/strong&gt; I would like to know whether 4.2 holds outside dev.to, or whether this whole thing is even more corpus-dependent than I think.&lt;/p&gt;

&lt;p&gt;The raw data (10,000 rows: kind, chars, tokens, ratio, error) is in the repo with the script that produced it. Rerun it on your own text; that takes five minutes and beats trusting my constant.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Count GPT Tokens Before You Hit the API</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:48:50 +0000</pubDate>
      <link>https://dev.to/devenquan/how-to-count-gpt-tokens-before-you-hit-the-api-2p57</link>
      <guid>https://dev.to/devenquan/how-to-count-gpt-tokens-before-you-hit-the-api-2p57</guid>
      <description>&lt;p&gt;You cannot control what you do not measure — and GPT APIs bill you for something you never see.&lt;/p&gt;

&lt;p&gt;Every request is priced, rate-limited, and truncated by &lt;strong&gt;token count&lt;/strong&gt;, not by words, characters, or number of calls. If you only learn the real number from the API response, you already paid for it. Counting GPT tokens &lt;em&gt;before&lt;/em&gt; you send gives you three things: a cost you can quote, a context window you know you fit inside, and a prompt you can trim while trimming is still cheap.&lt;/p&gt;

&lt;p&gt;Here is how to do it in Python and JavaScript, which encoding to use, and what your request actually contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens are not words
&lt;/h2&gt;

&lt;p&gt;A token is a subword unit from a byte-pair encoding (BPE) tokenizer. Common English words usually become one token. Less common ones get split — &lt;code&gt;tokenization&lt;/code&gt; becomes &lt;code&gt;token&lt;/code&gt; + &lt;code&gt;ization&lt;/code&gt;, two tokens for one word.&lt;/p&gt;

&lt;p&gt;For rough planning, English prose runs about &lt;strong&gt;4 characters per token&lt;/strong&gt;, or roughly 0.75 words per token. That average breaks the moment the input stops looking like English prose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifiers, URLs, file paths&lt;/strong&gt; — a UUID is several tokens; every path segment splits separately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code&lt;/strong&gt; — indentation, punctuation, and operators each cost tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbers&lt;/strong&gt; — digits are often split into groups, so &lt;code&gt;2026&lt;/code&gt; may not be one token&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CJK text&lt;/strong&gt; — Chinese characters commonly cost 1–2 tokens each, so character heuristics undercount badly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emoji and accents&lt;/strong&gt; — expand into multiple tokens once UTF-8 encoded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pretty-printed JSON&lt;/strong&gt; — indentation across a big object adds up fast; collapse it before the call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also cannot estimate in the other direction. A character or word count gives you a ceiling, not the billed number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which encoding your model uses
&lt;/h2&gt;

&lt;p&gt;Counts differ between tokenizer generations. Using the wrong one gives you a plausible number that is off by 10–20% on English text and much more on anything else.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Encoding&lt;/th&gt;
&lt;th&gt;Vocab (approx.)&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;o200k_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~200,000&lt;/td&gt;
&lt;td&gt;gpt-4o, gpt-4o-mini, gpt-4.1 family, gpt-5 family, o1, o3, o3-mini, o4-mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cl100k_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~100,000&lt;/td&gt;
&lt;td&gt;gpt-4, gpt-4-turbo, gpt-3.5-turbo, text-embedding-3-small/large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p50k_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~50,000&lt;/td&gt;
&lt;td&gt;text-davinci-002/003, code-davinci-002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;r50k_base&lt;/code&gt; (gpt2)&lt;/td&gt;
&lt;td&gt;~50,000&lt;/td&gt;
&lt;td&gt;gpt-2, davinci, curie, babbage, ada&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You rarely need to memorize it — tiktoken resolves the encoding from the model name by prefix matching, so anything starting with &lt;code&gt;gpt-5-&lt;/code&gt;, &lt;code&gt;gpt-4o-&lt;/code&gt;, &lt;code&gt;gpt-4.1-&lt;/code&gt;, &lt;code&gt;o1-&lt;/code&gt;, &lt;code&gt;o3-&lt;/code&gt;, or &lt;code&gt;o4-mini-&lt;/code&gt; resolves to &lt;code&gt;o200k_base&lt;/code&gt;.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o3-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encoding_for_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Count GPT tokens in Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tiktoken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encoding_for_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# resolves to o200k_base
&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs offline — no API key, no network request after the encoding file is cached.&lt;/p&gt;

&lt;p&gt;When a count surprises you, look at how the string actually splits:&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unbelievable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode_single_token_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# [59026, 17536]  -&amp;gt;  [b'Un', b'believable']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Counting a whole chat request
&lt;/h3&gt;

&lt;p&gt;Each message carries structural overhead beyond its content. This approximation follows OpenAI's cookbook: roughly 3 tokens per message, plus 1 if you set a &lt;code&gt;name&lt;/code&gt;, plus 3 priming the assistant reply.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_chat_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encoding_for_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o200k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# fallback for new model names
&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_per_name&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;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&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;+=&lt;/span&gt; &lt;span class="n"&gt;tokens_per_message&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&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;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&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;+=&lt;/span&gt; &lt;span class="n"&gt;tokens_per_name&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;  &lt;span class="c1"&gt;# assistant reply primer
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat it as a planning figure. The authoritative count is always the &lt;code&gt;usage&lt;/code&gt; object in the response — log it and reconcile against your estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count GPT tokens in JavaScript
&lt;/h2&gt;

&lt;p&gt;Counting where the prompt is built lets you warn users before they send something enormous, without shipping their text anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;js-tiktoken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;encodingForModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encodingForModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Count me before I cost you money.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gpt-tokenizer&lt;/code&gt; is the other common option and ships per-model entry points, which helps when bundle size matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-tokenizer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tokenizer is just a data table and a merge algorithm, so none of this needs a network call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your request actually contains
&lt;/h2&gt;

&lt;p&gt;The user message is usually the smallest part of the bill. All of this counts against the same context window and the same input price:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System prompt&lt;/strong&gt; — resent every request, so length here is a permanent tax&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation history&lt;/strong&gt; — grows quadratically with turns if replayed in full&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool/function schemas&lt;/strong&gt; — twenty tools with detailed descriptions can occupy thousands of tokens even when none are called&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieved context&lt;/strong&gt; — every pasted chunk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning tokens&lt;/strong&gt; — on reasoning models these occupy context and bill as output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reply&lt;/strong&gt; — output usually costs several times more per token than input, and &lt;code&gt;max_tokens&lt;/code&gt; is a cap, not a budget&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Budgeting the context window
&lt;/h3&gt;

&lt;p&gt;The window is shared, not per-message:&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;free_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context_window&lt;/span&gt;
            &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;retrieved_chunks&lt;/span&gt;
            &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;max_tokens_you_allow_for_the_reply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that lands near zero you get either a rejected request or silently dropped messages — your "memory" vanishes mid-conversation. Keep 10–15% slack so unusual inputs do not tip you over. Better: size retrieval from the count. After counting the fixed parts, whatever remains is exactly how many tokens of context you can afford.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning tokens into money
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;input_price&lt;/span&gt;
     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;output_price&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard-tier OpenAI list prices, October 2026 (always re-check &lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;the official pricing page&lt;/a&gt;):&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;Input / 1M&lt;/th&gt;
&lt;th&gt;Cached 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;gpt-5-nano&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;$0.005&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5-mini&lt;/td&gt;
&lt;td&gt;$0.25&lt;/td&gt;
&lt;td&gt;$0.025&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o-mini&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;td&gt;$0.075&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o3&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$8.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Concrete case: 10,000 support answers per day, 600-token prompt, 300-token reply.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gpt-4o-mini:&lt;/strong&gt; 6M input × $0.15 = $0.90/day + 3M output × $0.60 = $1.80/day → &lt;strong&gt;$2.70/day&lt;/strong&gt; (~$81/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gpt-4o:&lt;/strong&gt; 6M × $2.50 = $15.00/day + 3M × $10.00 = $30.00/day → &lt;strong&gt;$45.00/day&lt;/strong&gt; (~$1,350/month)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same prompt, same answer quality for a routing task, &lt;strong&gt;16× difference&lt;/strong&gt;. This is why counting comes before model selection — you cannot compare prices without knowing volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six traps that inflate prompts
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Replaying the whole history&lt;/strong&gt; — summarize old turns or use a rolling window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minifying JSON by hand&lt;/strong&gt; — whitespace is real money at scale; strip it, drop unused field names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registering every tool on every request&lt;/strong&gt; — send only this turn's schemas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting one "average tokens per request" number&lt;/strong&gt; — track mean, p90, p99; the p99 breaks your budget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting character heuristics for non-English text&lt;/strong&gt; — 4 chars/token is an English figure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting &lt;code&gt;max_tokens&lt;/code&gt; to the max&lt;/strong&gt; — an uncapped reply is the classic surprise bill&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Checklist before every production request
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Count GPT tokens with the encoding of the model you actually serve&lt;/li&gt;
&lt;li&gt;Include system prompt, history, tools, and retrieved chunks — not just the user message&lt;/li&gt;
&lt;li&gt;Leave headroom: prompt + expected reply &amp;lt; context window&lt;/li&gt;
&lt;li&gt;Log response &lt;code&gt;usage&lt;/code&gt; and compare against your estimate weekly&lt;/li&gt;
&lt;li&gt;Hard-cap both input length and &lt;code&gt;max_tokens&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Re-check prices quarterly and after any model-family switch&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This post is the written-up version of how I actually budget prompts before shipping anything that calls an LLM. The full guide with extra sections is on my site: &lt;a href="https://aisubtools.xyz/how-to-count-gpt-tokens/" rel="noopener noreferrer"&gt;How to Count GPT Tokens Before You Hit the API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you work in the browser, two tools from the same toolkit are useful alongside this: a &lt;a href="https://aisubtools.xyz/json-formatter-validator/" rel="noopener noreferrer"&gt;JSON Formatter &amp;amp; Validator&lt;/a&gt; for collapsing whitespace out of payloads, and a &lt;a href="https://aisubtools.xyz/word-character-counter-tool/" rel="noopener noreferrer"&gt;Word &amp;amp; Character Counter&lt;/a&gt; for the fast ceiling estimate. Both run locally — nothing uploaded.&lt;/p&gt;

&lt;p&gt;How do you budget tokens in your pipeline — pre-flight counting, response &lt;code&gt;usage&lt;/code&gt; logging, or something else?&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cloudflare Just Shipped Post-Quantum at Internet Scale. Do You Need to Change Your Tools?</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:19:23 +0000</pubDate>
      <link>https://dev.to/devenquan/cloudflare-just-shipped-post-quantum-at-internet-scale-do-you-need-to-change-your-tools-p38</link>
      <guid>https://dev.to/devenquan/cloudflare-just-shipped-post-quantum-at-internet-scale-do-you-need-to-change-your-tools-p38</guid>
      <description>&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%2Fidv050zl6y95mcq7a49p.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%2Fidv050zl6y95mcq7a49p.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
On September 8, 2026, &lt;a href="https://blog.cloudflare.com/automatic-key-exchange-for-origins/" rel="noopener noreferrer"&gt;Cloudflare flipped the switch on post-quantum TLS origin handshakes&lt;/a&gt; across its edge. Two days later, on September 10, &lt;a href="https://blog.cloudflare.com/post-quantum-dnssec-1111/" rel="noopener noreferrer"&gt;1.1.1.1 enabled post-quantum DNSSEC&lt;/a&gt; powered by &lt;a href="https://csrc.nist.gov/pubs/fips/204/final" rel="noopener noreferrer"&gt;NIST's ML-DSA-44 algorithm&lt;/a&gt;. The numbers are not small: the TLS deployment alone now covers roughly &lt;strong&gt;45 billion daily connections&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you saw that headline and immediately opened your code editor to grep for &lt;code&gt;md5&lt;/code&gt; and &lt;code&gt;sha256&lt;/code&gt; — this post is for you. If you saw it and shrugged because you only use hashing for "checksumming" — this post is &lt;em&gt;also&lt;/em&gt; for you.&lt;/p&gt;

&lt;p&gt;If that's you — this is roughly what your terminal looks like right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"md5|sha256"&lt;/span&gt; src/ &lt;span class="nt"&gt;--color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
src/main.c:1:    fprintf&lt;span class="o"&gt;(&lt;/span&gt;stderr, &lt;span class="s2"&gt;"ERROR: Unsupported hash function: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;, hash_type&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
src/CMakeLists.txt:21:    target_compile_options&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; PRIVATE
                                                     &lt;span class="nt"&gt;-Werror&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deprecated-declarations&lt;span class="o"&gt;)&lt;/span&gt;
src/Rust/Cargo.toml:45:    version &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.1.1"&lt;/span&gt;
src/Rust/Cargo.toml:46:    md5    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.10.0"&lt;/span&gt;
src/Rust/Cargo.toml:47:    sha256 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.9.0"&lt;/span&gt;
src/Rust/src/lib.rs:120:   &lt;span class="nb"&gt;let hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; md5::compute&lt;span class="o"&gt;(&lt;/span&gt;data&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
src/Rust/src/lib.rs:11:    &lt;span class="nb"&gt;let hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; sha2::Sha256::digest&lt;span class="o"&gt;(&lt;/span&gt;data&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
src/scripts/build.sh:15:    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; openssl   &amp;amp;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;src/scripts/build.sh:15:    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &amp;amp;&amp;gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
&lt;/span&gt;src/scripts/build.sh:12:        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: Neither openssl nor sha256sum found!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
src/scripts/util.sh:35:    &lt;span class="nb"&gt;md5sum&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
src/scripts/util.sh:36:    &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That's not a snippet from a real repo, but the panic it captures is.)&lt;/p&gt;

&lt;p&gt;The short version: most of the tools you already use are fine. The panic comes from confusing &lt;strong&gt;hashes&lt;/strong&gt; with &lt;strong&gt;encryption&lt;/strong&gt;, and from using &lt;strong&gt;encryption&lt;/strong&gt; when you actually needed a &lt;strong&gt;checksum&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's untangle that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panic that doesn't help
&lt;/h2&gt;

&lt;p&gt;Here's what happened in a few Discord servers I read this week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should I migrate from MD5 to SHA-256? Grover's algorithm makes SHA-256 only 128-bit secure anyway. Should I just go to SHA-3?"&lt;/p&gt;

&lt;p&gt;"Should I rotate all my UUIDs to UUIDv7?"&lt;/p&gt;

&lt;p&gt;"Is base64 still safe?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are reasonable questions if you don't know the answer. They are also three different categories of mistake.&lt;/p&gt;

&lt;p&gt;MD5, SHA-1, and SHA-256 are &lt;strong&gt;hash functions&lt;/strong&gt;. They produce a fixed-size fingerprint from any input. The reason they exist is not to keep secrets — it's to verify that data didn't change in transit. A 1GB ISO file you downloaded can be checksummed with any of them; the question is "did the bytes arrive intact", not "can someone read the bytes".&lt;/p&gt;

&lt;p&gt;Base64 is &lt;strong&gt;an encoding&lt;/strong&gt;, not encryption. It exists so that binary data can travel through channels (JSON, URL paths, email) that only support text. There is no key. There is no "secure" mode. Anyone who tells you base64 is "encrypted" is selling you snake oil.&lt;/p&gt;

&lt;p&gt;UUID v4 is a &lt;strong&gt;random identifier&lt;/strong&gt;, 122 bits of randomness. It's not a hash, not an encryption — it's a label. Whether quantum computers threaten it depends on whether you used a cryptographic random number generator (good) or &lt;code&gt;Math.random()&lt;/code&gt; (you have bigger problems than Grover).&lt;/p&gt;

&lt;p&gt;So when Cloudflare ships post-quantum TLS, here's what's actually happening at the protocol level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TLS handshake negotiates session keys using a key exchange algorithm that a quantum computer cannot efficiently break.&lt;/li&gt;
&lt;li&gt;Everything &lt;strong&gt;inside&lt;/strong&gt; that TLS tunnel — your HTTP traffic, your JSON, your base64-encoded blobs — is still protected by classical symmetric ciphers (AES-256-GCM, ChaCha20) and authenticated by classical hashes (SHA-256, SHA-384).&lt;/li&gt;
&lt;li&gt;The risk model is "harvest now, decrypt later": an adversary records encrypted traffic today, waits for a cryptographically-relevant quantum computer (CRQC), then decrypts it. That's the threat post-quantum key exchange is closing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your MD5 checksum tool does not enter this picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each tool is actually for
&lt;/h2&gt;

&lt;p&gt;Since I built &lt;a href="https://aisubtools.xyz/" rel="noopener noreferrer"&gt;AI Subtools&lt;/a&gt; — a no-fluff browser-side toolbox — here's the honest guide. Same tool, different intent:&lt;/p&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 does&lt;/th&gt;
&lt;th&gt;Threat model&lt;/th&gt;
&lt;th&gt;Replace with PQC?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRC32 Checksum Generator&lt;/td&gt;
&lt;td&gt;Fast integrity check on small files / packets&lt;/td&gt;
&lt;td&gt;Accidental corruption&lt;/td&gt;
&lt;td&gt;No — error-detection, not security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD5 Hash Generator&lt;/td&gt;
&lt;td&gt;Legacy checksum, fingerprint for deduplication&lt;/td&gt;
&lt;td&gt;Accidental corruption&lt;/td&gt;
&lt;td&gt;No, for the same reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA256 Hash Calculator&lt;/td&gt;
&lt;td&gt;Strong fingerprint for file integrity / commit hashes&lt;/td&gt;
&lt;td&gt;Accidental corruption&lt;/td&gt;
&lt;td&gt;Optional — SHA-256 is fine for non-adversarial checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base64 Encoder/Decoder&lt;/td&gt;
&lt;td&gt;Binary ↔ text transport&lt;/td&gt;
&lt;td&gt;None — it's encoding&lt;/td&gt;
&lt;td&gt;No — never was security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL Encoder/Decoder&lt;/td&gt;
&lt;td&gt;Make strings safe for URLs&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v4 Generator&lt;/td&gt;
&lt;td&gt;Random 122-bit ID&lt;/td&gt;
&lt;td&gt;Collision, predictability&lt;/td&gt;
&lt;td&gt;No, if you use &lt;code&gt;crypto.getRandomValues&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password Strength Check&lt;/td&gt;
&lt;td&gt;Estimate entropy of a passphrase&lt;/td&gt;
&lt;td&gt;Dictionary attack&lt;/td&gt;
&lt;td&gt;The &lt;em&gt;length&lt;/em&gt; matters, not the hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random Password Generator&lt;/td&gt;
&lt;td&gt;High-entropy secrets&lt;/td&gt;
&lt;td&gt;Predictability&lt;/td&gt;
&lt;td&gt;No, if CSPRNG-backed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt/Decrypt (AES-256)&lt;/td&gt;
&lt;td&gt;Symmetric encryption, browser-side&lt;/td&gt;
&lt;td&gt;Adversary steals ciphertext&lt;/td&gt;
&lt;td&gt;No — AES-256 is post-quantum-safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the only one where a quantum computer would matter at all, and even then: &lt;a href="https://csrc.nist.gov/Pubs/ir/8105/Final" rel="noopener noreferrer"&gt;Grover's algorithm gives a quadratic speedup, which means AES-256 effectively becomes AES-128&lt;/a&gt; — still considered computationally infeasible to break. NIST standardized it for exactly that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when &lt;em&gt;do&lt;/em&gt; you actually need post-quantum?
&lt;/h2&gt;

&lt;p&gt;There are exactly three situations where PQC migration matters for the average developer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You run a TLS server.&lt;/strong&gt; Migrate your server library to support hybrid Kyber/X25519 key exchange (OpenSSL 3.5+, BoringSSL, rustls 0.23+). Cloudflare already did this for you if your users connect via their edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You sign software or firmware updates.&lt;/strong&gt; Classic ECDSA signatures are quantum-broken. Migrate to ML-DSA-44 / Dilithium for new releases; consider re-signing historical binaries if your threat model includes harvest-now-decrypt-later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You embed long-lived secrets in firmware or code signing certificates.&lt;/strong&gt; Anything with a 10+ year shelf life needs a PQC plan now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else — your file checksums, your base64-encoded JSON payloads, your UUIDs, your SHA-256 commit hashes — is &lt;strong&gt;already fine&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple decision tree
&lt;/h2&gt;

&lt;p&gt;When you reach for one of these tools, ask one question: &lt;strong&gt;am I protecting against bit-flips, or against an adversary?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bit-flips only&lt;/strong&gt; (corrupted download, broken pipe, accidental edit) → CRC32, MD5, SHA-256. Any of them. CRC32 is fastest; SHA-256 is most defensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary who can read but not modify&lt;/strong&gt; → AES-256 encryption. Done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary who can read and modify&lt;/strong&gt; → authenticated encryption (AES-GCM) or HMAC over the ciphertext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversary with a quantum computer who recorded your traffic years ago&lt;/strong&gt; → PQC key exchange at the protocol layer (already handled by Cloudflare, AWS, GCP, Fastly, Akamai in 2026).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No framework. No vendor lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is MD5 broken?&lt;/strong&gt;&lt;br&gt;
Yes, but not for checksumming. Collision attacks against MD5 mean you can craft two different files with the same MD5 — useful for forging signatures, useless for catching accidental corruption. Use MD5 for cache keys, deduplication, and legacy file verification. Don't use it for digital signatures — that hasn't been safe since 2008.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I switch to SHA-3 for file integrity?&lt;/strong&gt;&lt;br&gt;
No reason to. SHA-256 is fine for non-adversarial integrity. The actual quantum risk lands on &lt;strong&gt;digital signatures&lt;/strong&gt; and &lt;strong&gt;key exchange&lt;/strong&gt;, not on file checksums.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is base64 encryption?&lt;/strong&gt;&lt;br&gt;
No. It is an encoding. There is no key. Anyone — including this page's CSS — can decode it. If someone tells you they "encrypted it with base64", they did not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is UUID v4 quantum-safe?&lt;/strong&gt;&lt;br&gt;
UUID v4 is 122 bits from a CSPRNG. Brute-forcing 122 bits is computationally infeasible even with Grover's algorithm — you'd need ~2^61 operations, which is more atoms than in a kilogram of lead. As long as your UUID generator uses a cryptographic random source, you're fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What about AES-256?&lt;/strong&gt;&lt;br&gt;
Post-quantum-safe by design. Grover halves effective key length to 128 bits. NIST confirmed AES-256 is acceptable for top-secret data through the post-quantum era.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Cloudflare's PQC deployment this week was an infrastructure milestone, not a consumer panic trigger. The right response is to make sure your &lt;strong&gt;TLS library&lt;/strong&gt; supports hybrid post-quantum key exchange — not to throw out your hash functions.&lt;/p&gt;

&lt;p&gt;If you're picking a tool for a one-off task today, pick the one that matches the threat model, not the trend.&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;Built and maintained at &lt;a href="https://aisubtools.xyz/" rel="noopener noreferrer"&gt;aisubtools.xyz&lt;/a&gt; — 40+ free browser-side tools for developers and creators.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>postquantum</category>
      <category>cloudflare</category>
      <category>security</category>
    </item>
    <item>
      <title>Open-sourcing my dev tools as plain HTML files (no npm, no build)</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:48:49 +0000</pubDate>
      <link>https://dev.to/devenquan/open-sourcing-my-dev-tools-as-plain-html-files-no-npm-no-build-3ga7</link>
      <guid>https://dev.to/devenquan/open-sourcing-my-dev-tools-as-plain-html-files-no-npm-no-build-3ga7</guid>
      <description>&lt;p&gt;&lt;br&gt;
```Last month I shipped three developer utilities — a Base64 encoder, a Unix&lt;br&gt;
timestamp converter, and a CSS minifier. Each one runs100% in your browser.&lt;br&gt;
No servers, no uploads, no analytics, no tracking.&lt;br&gt;
\&lt;br&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%2Fxz3a4k0dhyu11dp9yxrn.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%2Fxz3a4k0dhyu11dp9yxrn.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yesterday I open-sourced them on GitHub. This post is the story of &lt;em&gt;why&lt;/em&gt; I&lt;br&gt;
built them this way and &lt;em&gt;why&lt;/em&gt; I open-sourced them at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "free online tools"
&lt;/h2&gt;

&lt;p&gt;Most "free online tools" secretly upload your input to a server, process it&lt;br&gt;
there, and hope nobody keeps a copy. That is a terrible default.&lt;/p&gt;

&lt;p&gt;Try this experiment: paste &lt;code&gt;你好世界 🌍&lt;/code&gt; into the most popular online Base64&lt;br&gt;
encoder. About half of them will return &lt;code&gt;å¥½ä¸ç•Œ&lt;/code&gt; — broken output that no&lt;br&gt;
longer decodes back to what you typed. Why? Because most sites treat text as&lt;br&gt;
Latin-1 by accident.&lt;/p&gt;

&lt;p&gt;Even when they work correctly, you're trusting that the site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;won't log your input&lt;/li&gt;
&lt;li&gt;won't sell it to a third party&lt;/li&gt;
&lt;li&gt;won't get breached next year&lt;/li&gt;
&lt;li&gt;won't disappear and take your workflow with it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source fixes all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://aisubtools.xyz" rel="noopener noreferrer"&gt;aisubtools.xyz&lt;/a&gt; — a collection of free online&lt;br&gt;
utilities where nothing ever leaves your browser. Yesterday I extracted&lt;br&gt;
three of them into a standalone open-source repo:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github.com/enquannacc/aisubtools-browser-tools" rel="noopener noreferrer"&gt;github.com/enquannacc/aisubtools-browser-tools&lt;/a&gt;&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;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base64 encoder/decoder&lt;/td&gt;
&lt;td&gt;Handles UTF-8 correctly via &lt;code&gt;TextEncoder&lt;/code&gt; / &lt;code&gt;TextDecoder&lt;/code&gt;. No mojibake.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unix timestamp converter&lt;/td&gt;
&lt;td&gt;Bidirectional conversion with local and UTC time, plus a live clock.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS minifier&lt;/td&gt;
&lt;td&gt;Regex-based whitespace stripper with before/after size savings.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MIT licensed. No &lt;code&gt;npm install&lt;/code&gt;. No build step. No dependencies. Open the file,&lt;br&gt;
use it, close the tab. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: one HTML file per tool
&lt;/h2&gt;

&lt;p&gt;Every tool in the repo is a single self-contained HTML file. Here's the&lt;br&gt;
pattern I used across all three:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
html
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset="utf-8"&amp;gt;
  &amp;lt;title&amp;gt;Base64 Encode / Decode — Browser Tool&amp;lt;/title&amp;gt;
  &amp;lt;style&amp;gt;
    /* ~100 lines of CSS — nothing fancy, system fonts, CSS variables */
 &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;Base64 Encode / Decode&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;Runs 100% in your browser — nothing is uploaded anywhere.&amp;lt;/p&amp;gt;
  &amp;lt;textarea id="input" placeholder="Type or paste here"&amp;gt;&amp;lt;/textarea&amp;gt;
  &amp;lt;button id="convert"&amp;gt;Convert&amp;lt;/button&amp;gt;
  &amp;lt;textarea id="output" readonly&amp;gt;&amp;lt;/textarea&amp;gt;
  &amp;lt;script&amp;gt;
    const input = document.getElementById('input');
    const output = document.getElementById('output');
    const convert = document.getElementById('convert');

    let mode = 'encode';

    function utf8ToBase64(str) {
      return btoa(String.fromCharCode(
        ...new TextEncoder().encode(str)
      ));
    }

    function base64ToUtf8(b64) {
      const bytes = Uint8Array.from(atob(b64), c =&amp;gt; c.charCodeAt(0));
      return new TextDecoder().decode(bytes);
    }

    convert.addEventListener('click', () =&amp;gt; {
      try {
        output.value = mode === 'encode'
          ? utf8ToBase64(input.value)
          : base64ToUtf8(input.value.trim());
      } catch (e) {
        output.value = 'Error: ' + e.message;
      }
    });
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;




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

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why %20 Keeps Appearing in Your URLs: A Practical Guide to URL Encoding</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Sun, 06 Sep 2026 14:19:43 +0000</pubDate>
      <link>https://dev.to/devenquan/why-20-keeps-appearing-in-your-urls-a-practical-guide-to-url-encoding-4b4l</link>
      <guid>https://dev.to/devenquan/why-20-keeps-appearing-in-your-urls-a-practical-guide-to-url-encoding-4b4l</guid>
      <description>&lt;p&gt;You paste a URL into your browser, hit Enter, and suddenly it's full of &lt;code&gt;%20&lt;/code&gt;, &lt;code&gt;%3F&lt;/code&gt;, and &lt;code&gt;%26&lt;/code&gt;. Or worse — your API call fails because a query parameter contains a space or an &lt;code&gt;&amp;amp;&lt;/code&gt; you didn't encode.&lt;/p&gt;

&lt;p&gt;URL encoding trips up everyone eventually. The good news: it's much simpler than it looks. This is a practical guide to reading percent-encoded URLs, knowing when encoding happens automatically, and fixing the bugs it causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why URL encoding exists at all
&lt;/h2&gt;

&lt;p&gt;A URL has to be plain ASCII — but the real world isn't. Spaces, emoji, Chinese characters, ampersands... all of them need to travel through a URL &lt;strong&gt;without breaking its structure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The solution: any character that isn't URL-safe gets replaced with a &lt;code&gt;%&lt;/code&gt; followed by its hexadecimal byte value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A space becomes &lt;code&gt;%20&lt;/code&gt; (hex 20 = 32 = space in ASCII)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;&lt;/code&gt; becomes &lt;code&gt;%26&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;?&lt;/code&gt; becomes &lt;code&gt;%3F&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;&amp;amp;&lt;/code&gt; are the interesting ones — because those characters &lt;em&gt;do&lt;/em&gt; have a special job in URLs. That's the key to understanding the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two categories you must not confuse
&lt;/h2&gt;

&lt;p&gt;Every character in a URL falls into one of two buckets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reserved characters&lt;/strong&gt; — they are part of the URL's &lt;em&gt;structure&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;: / ? # [ ] @ ! $ &amp;amp; ' ( ) * + , ; =
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These have meaning: &lt;code&gt;?&lt;/code&gt; starts the query string, &lt;code&gt;&amp;amp;&lt;/code&gt; separates parameters, &lt;code&gt;#&lt;/code&gt; starts the fragment. If your &lt;em&gt;data&lt;/em&gt; contains one of these characters, it &lt;strong&gt;must&lt;/strong&gt; be encoded — otherwise the URL's structure gets corrupted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Unsafe / non-ASCII characters&lt;/strong&gt; — spaces, control characters, non-Latin text, emoji. They must also be encoded because URLs are transmitted as ASCII.&lt;/p&gt;

&lt;p&gt;Everything else (letters, digits, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;) passes through untouched.&lt;/p&gt;

&lt;p&gt;That's the entire mental model. When you see &lt;code&gt;%26&lt;/code&gt; in a URL, it doesn't mean "the website hates you" — it means the original data contained a literal &lt;code&gt;&amp;amp;&lt;/code&gt; that had to be protected so it wouldn't be mistaken for a parameter separator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheat sheet you'll actually use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Encoded&lt;/th&gt;
&lt;th&gt;Character&lt;/th&gt;
&lt;th&gt;Where you'll meet it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;space&lt;/td&gt;
&lt;td&gt;Search queries, file names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%2F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paths inside query params&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%3F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A literal &lt;code&gt;?&lt;/code&gt; in data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%26&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values containing &lt;code&gt;&amp;amp;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%3D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values containing &lt;code&gt;=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The classic double-encode bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%2B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy form encoding of spaces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;About &lt;code&gt;%25&lt;/code&gt;:&lt;/strong&gt; this is my favorite interview question. If you see &lt;code&gt;%2520&lt;/code&gt; in a URL, the original data was &lt;code&gt;%20&lt;/code&gt; — someone encoded an &lt;em&gt;already encoded&lt;/em&gt; string. The &lt;code&gt;%&lt;/code&gt; became &lt;code&gt;%25&lt;/code&gt;, leaving &lt;code&gt;20&lt;/code&gt; intact. Double-encoding bugs almost always trace back to a URL being encoded twice by two different layers (your code + a library).&lt;/p&gt;

&lt;h2&gt;
  
  
  When encoding happens automatically (and when it doesn't)
&lt;/h2&gt;

&lt;p&gt;This is where bugs sneak in. The browser encodes some things for you, but not everything, and not everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser address bar is forgiving.&lt;/strong&gt; Type &lt;code&gt;https://example.com/search?q=hello world&lt;/code&gt; and the browser will quietly encode the space for you. This hides bugs during manual testing that blow up when the same URL is requested programmatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetch()&lt;/code&gt; and &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; are NOT forgiving in the same way.&lt;/strong&gt; Spaces and non-ASCII characters in a query string can produce malformed requests or, worse, silently work in one browser and fail in another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form submissions (&lt;code&gt;GET&lt;/code&gt; forms) encode spaces as &lt;code&gt;+&lt;/code&gt;.&lt;/strong&gt; That's why &lt;code&gt;hello+world&lt;/code&gt; and &lt;code&gt;hello%20world&lt;/code&gt; can both mean "hello world", depending on which layer produced them. This is also why decoding a form-encoded string with the wrong decoder gives you literal &lt;code&gt;+&lt;/code&gt; signs scattered through your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JavaScript functions, once and for all
&lt;/h2&gt;

&lt;p&gt;In JavaScript you have four tools, and picking the wrong one is a rite of passage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// encodeURIComponent — for a VALUE going into a query string&lt;/span&gt;
&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish &amp;amp; chips? yes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "fish%20%26%20chips%3F%20yes"&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Encodes &amp;amp; and ? — the structure is protected&lt;/span&gt;

&lt;span class="c1"&gt;// encodeURI — for a WHOLE URL that's already assembled&lt;/span&gt;
&lt;span class="nf"&gt;encodeURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/search?q=hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "https://example.com/search?q=hello%20world"&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Leaves :// and ? alone, encodes the space&lt;/span&gt;

&lt;span class="c1"&gt;// decodeURIComponent / decodeURI — the reversals&lt;/span&gt;
&lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish%20%26%20chips&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "fish &amp;amp; chips"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encoding one value&lt;/strong&gt; (about to insert it into a query string)? → &lt;code&gt;encodeURIComponent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding a complete URL&lt;/strong&gt; that's already well-formed except for stray spaces/unicode? → &lt;code&gt;encodeURI&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake I see most often: using &lt;code&gt;encodeURI&lt;/code&gt; on a value. It leaves &lt;code&gt;&amp;amp;&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt; untouched, so &lt;code&gt;fish &amp;amp; chips&lt;/code&gt; slips into the URL as-is and silently truncates your query string at the &lt;code&gt;&amp;amp;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Broken: value contains &amp;amp; and ?&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/q?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish &amp;amp; chips?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Correct&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/q?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fish &amp;amp; chips?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three real bugs you'll eventually meet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The truncated query string.&lt;/strong&gt; A search for &lt;code&gt;Tom &amp;amp; Jerry&lt;/code&gt; becomes &lt;code&gt;?q=Tom&lt;/code&gt; and everything after &lt;code&gt;&amp;amp;&lt;/code&gt; becomes a new (empty) parameter. The page loads, the search just returns wrong results. Maddening until you look at the Network tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The double-encoded redirect.&lt;/strong&gt; You pass a &lt;code&gt;?next=&lt;/code&gt; parameter containing a full URL. Some middleware encodes it again. The redirect target now contains &lt;code&gt;%253A&lt;/code&gt; instead of &lt;code&gt;%3A&lt;/code&gt;. The fix is to decode exactly once at each layer, and to be disciplined about &lt;em&gt;which&lt;/em&gt; layer owns encoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The emoji that kills your sitemap.&lt;/strong&gt; Non-ASCII characters in URLs are legal (via encoding), but if your CMS or export script writes raw UTF-8 into an XML sitemap without encoding, Google may start complaining. Always encode before writing URLs into structured files.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you just need to check one string quickly
&lt;/h2&gt;

&lt;p&gt;For debugging, I keep a browser-side &lt;a href="https://aisubtools.xyz/url-encoder-decoder-online-tool/" rel="noopener noreferrer"&gt;URL encoder/decoder&lt;/a&gt; bookmarked — paste the string, see it encoded and decoded side by side, copy whichever form you need. It runs client-side, so tokens and internal URLs never leave the browser. Pair it with &lt;code&gt;console.log&lt;/code&gt; and the Network tab and you can diagnose 95% of encoding bugs in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;%XX&lt;/code&gt; is just a character's hex byte value, prefixed with &lt;code&gt;%&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reserved characters (&lt;code&gt;&amp;amp; ? = / #&lt;/code&gt;) must be encoded when they're &lt;em&gt;data&lt;/em&gt;, not structure&lt;/li&gt;
&lt;li&gt;The address bar hides encoding bugs; test with &lt;code&gt;fetch&lt;/code&gt; or curl&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;encodeURIComponent&lt;/code&gt; for values, &lt;code&gt;encodeURI&lt;/code&gt; for whole URLs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%25&lt;/code&gt; appearing where &lt;code&gt;%20&lt;/code&gt; should be means something double-encoded&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;URL encoding stops being scary the moment you stop reading &lt;code&gt;%3F&lt;/code&gt; as noise and start reading it as "that's a literal question mark in the data". Once that clicks, every encoded URL you see becomes just... a string with a hat on.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions or war stories about encoding bugs? &lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built 35+ Browser-Side Tools Because I Was Tired of Uploading My Data to Random Servers</title>
      <dc:creator>Zhang Enquan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:50:11 +0000</pubDate>
      <link>https://dev.to/devenquan/i-built-35-browser-side-tools-because-i-was-tired-of-uploading-my-data-to-random-servers-4h9n</link>
      <guid>https://dev.to/devenquan/i-built-35-browser-side-tools-because-i-was-tired-of-uploading-my-data-to-random-servers-4h9n</guid>
      <description>&lt;p&gt;Every time I needed to format a JSON payload or compress an image, I found myself on some random tool site with three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My data went to their server.&lt;/strong&gt; Paste a password or an API token into a "hash generator" and you're just trusting that site not to log it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ads everywhere.&lt;/strong&gt; The tool was buried under banners, and half the buttons were ad-click traps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-up walls.&lt;/strong&gt; Want to download your converted file? Create an account first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built &lt;a href="https://aisubtools.xyz" rel="noopener noreferrer"&gt;AI SubTools&lt;/a&gt; — a collection of 35+ free tools where &lt;strong&gt;everything runs client-side in JavaScript&lt;/strong&gt;. No uploads. No accounts. No ads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side matters
&lt;/h2&gt;

&lt;p&gt;It's not just a privacy stance. When the tool runs in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's instant.&lt;/strong&gt; No round-trip to a server, no queue. Image compression happens as fast as your CPU allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works offline&lt;/strong&gt; once the page is loaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data never leaves your device.&lt;/strong&gt; For tools that touch passwords, tokens, or checksums, this should be the default, not a feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Some of the more interesting tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cron expression generator &amp;amp; tester&lt;/strong&gt; — with plain-English explanations of what your expression actually does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG background remover&lt;/strong&gt; — runs entirely in-browser, no upload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robots.txt + XML sitemap generator&lt;/strong&gt; — for the SEO-minded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAN/UPC barcode creator&lt;/strong&gt; — for e-commerce sellers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON formatter &amp;amp; validator, Base64 encoder/decoder, MD5/SHA-256 generators&lt;/strong&gt; — the daily-driver classics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The technical trade-offs
&lt;/h2&gt;

&lt;p&gt;The hardest parts were making &lt;strong&gt;image compression&lt;/strong&gt; and &lt;strong&gt;background removal&lt;/strong&gt; perform well without WASM. Canvas-based processing is fast enough for most images, but there's a real art to avoiding UI freezes on large files — chunked processing and careful memory management matter more than raw algorithm speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The site is a side project that grew out of my own daily frustration. New tools are added regularly, and I'm currently prioritizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More file-format converters&lt;/li&gt;
&lt;li&gt;Better mobile experience for the image tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What tools would you want added?&lt;/strong&gt; I'd genuinely love to hear what's missing from your daily workflow — the best suggestions will get built.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. Everything is free, forever. No premium tier hiding the useful stuff.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
