<?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: Anoop Kumar</title>
    <description>The latest articles on DEV Community by Anoop Kumar (@anoop_kumar_63925e275ea06).</description>
    <link>https://dev.to/anoop_kumar_63925e275ea06</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%2F2450670%2F40e0d6c8-d995-4305-807f-20df88e08990.jpeg</url>
      <title>DEV Community: Anoop Kumar</title>
      <link>https://dev.to/anoop_kumar_63925e275ea06</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anoop_kumar_63925e275ea06"/>
    <language>en</language>
    <item>
      <title>System Design Fundamentals I Wish I Had Learned Earlier</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:17:19 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/system-design-fundamentals-i-wish-i-had-learned-earlier-2o8h</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/system-design-fundamentals-i-wish-i-had-learned-earlier-2o8h</guid>
      <description>&lt;p&gt;Most system design resources focus on interview preparation — how to design Twitter in 45 minutes, how to draw boxes and arrows convincingly. This article is different. These are the concepts I wish I had internalized earlier because they changed how I approach every technical decision, including the ones I make building &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fundamental trade-off: consistency vs availability
&lt;/h2&gt;

&lt;p&gt;The CAP theorem states that a distributed system can guarantee at most two of three properties: Consistency, Availability, and Partition tolerance. Since network partitions are unavoidable in real systems, the practical trade-off is between consistency and availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt; means every read receives the most recent write. If user A writes data and user B immediately reads it, B sees A's write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Availability&lt;/strong&gt; means every request receives a response — not necessarily the most recent data, but a response.&lt;/p&gt;

&lt;p&gt;Most web applications choose availability over strict consistency, accepting eventual consistency — the guarantee that data will become consistent eventually, even if reads may return slightly stale data during the propagation window.&lt;/p&gt;

&lt;p&gt;The practical example: if you update your profile picture on a social platform, some users might see the old picture for a few seconds or minutes while the change propagates. This is acceptable. If your bank account balance is wrong for even one second, that is not acceptable.&lt;/p&gt;

&lt;p&gt;Knowing which side of this trade-off your application falls on should drive every database and caching decision you make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Horizontal vs vertical scaling — and when each applies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Vertical scaling&lt;/strong&gt; (scaling up) means adding more resources to a single machine — more CPU, more RAM, faster storage. It is simple, requires no architectural changes, and works until you hit the hardware limit or the cost becomes prohibitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal scaling&lt;/strong&gt; (scaling out) means adding more machines and distributing load across them. It requires your application to be stateless — requests from the same user can be handled by any server.&lt;/p&gt;

&lt;p&gt;The question developers often ask is "which should I use?" The correct answer is: vertical first, horizontal when necessary.&lt;/p&gt;

&lt;p&gt;Vertical scaling is dramatically simpler. You resize an instance and you are done. Horizontal scaling requires load balancers, session management, distributed caching, and careful thinking about where state lives.&lt;/p&gt;

&lt;p&gt;The mistake is assuming you need horizontal scaling before you have validated the need. A well-optimized single server can handle enormous traffic. Stack Overflow ran on five servers for years at significant scale. Optimize your code and your queries before you add machines.&lt;/p&gt;

&lt;p&gt;When you do need horizontal scaling, the key constraint is &lt;strong&gt;statelessness&lt;/strong&gt;. Your application servers must not store session state locally. Sessions go in Redis. Files go in object storage. Database writes go to a shared database. Each server is identical and interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching — the most important performance tool
&lt;/h2&gt;

&lt;p&gt;Caching is storing the result of an expensive computation so you can serve it quickly on subsequent requests. It is the single highest-leverage performance improvement in most systems.&lt;/p&gt;

&lt;p&gt;There are four places you can cache:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side (browser cache):&lt;/strong&gt; HTTP cache headers (&lt;code&gt;Cache-Control&lt;/code&gt;, &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Last-Modified&lt;/code&gt;) tell browsers how long to cache static assets. A properly configured CDN and cache policy means your CSS and JavaScript files are served from the user's local cache — zero network round-trips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN cache:&lt;/strong&gt; Content delivery networks cache responses at edge nodes geographically close to users. Dynamic content that does not change per-user — marketing pages, blog posts, public API responses — should be cached at the CDN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application cache (Redis/Memcached):&lt;/strong&gt; Server-side in-memory caching for database query results, computed aggregations, and anything expensive to regenerate. Redis is the standard choice — it is fast, supports data structures, and handles expiration automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database query cache:&lt;/strong&gt; Most databases cache query results internally. This is largely automatic, but it means repeated identical queries are fast and schema changes or cache invalidation can cause temporary performance drops.&lt;/p&gt;

&lt;p&gt;The hardest problem in caching is cache invalidation — knowing when to expire stale data. Three strategies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL (time-to-live):&lt;/strong&gt; Data expires after a fixed duration. Simple, but data may be stale within the TTL window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write-through:&lt;/strong&gt; When data is written to the database, the cache is updated simultaneously. Consistent, but every write hits both database and cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache-aside:&lt;/strong&gt; Application checks cache first. On a miss, reads from database and populates the cache. On a write, invalidates the cache entry. The most common pattern for read-heavy workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database indexing — what it is and why it matters
&lt;/h2&gt;

&lt;p&gt;Without an index, a database must scan every row in a table to find matching records. This is a full table scan, and its cost grows linearly with the number of rows.&lt;/p&gt;

&lt;p&gt;An index is a separate data structure (typically a B-tree) that allows the database to find matching rows in O(log n) time instead of O(n) time. For a table with one million rows, this is the difference between scanning 1,000,000 rows and scanning roughly 20.&lt;/p&gt;

&lt;p&gt;The columns to index:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary keys&lt;/strong&gt; — indexed automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign keys&lt;/strong&gt; — columns used in JOIN conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columns in WHERE clauses&lt;/strong&gt; — especially high-cardinality columns (many distinct values)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columns in ORDER BY&lt;/strong&gt; — if you frequently sort by a column
The trade-off: indexes make reads fast and writes slow. Every INSERT, UPDATE, or DELETE must also update every relevant index. For read-heavy applications (most web applications), this trade-off is almost always worth it. For write-heavy applications (logging, analytics ingestion), index carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Composite indexes — indexes on multiple columns — matter for query performance. An index on &lt;code&gt;(user_id, created_at)&lt;/code&gt; supports queries that filter by &lt;code&gt;user_id&lt;/code&gt; and sort by &lt;code&gt;created_at&lt;/code&gt;, but only if the columns appear in that order in the WHERE clause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message queues — decoupling for resilience
&lt;/h2&gt;

&lt;p&gt;A message queue is a buffer between a producer (something that generates work) and a consumer (something that processes work). The producer puts a message in the queue and moves on. The consumer reads from the queue and processes at its own pace.&lt;/p&gt;

&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling traffic spikes.&lt;/strong&gt; If your API receives 10,000 requests in one second, a queue absorbs the burst and processes it steadily rather than overwhelming your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decoupling services.&lt;/strong&gt; If the email service is down, you do not want to fail the user's registration request. Put the "send welcome email" task in a queue. When the email service recovers, it processes the queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry logic.&lt;/strong&gt; If a queue consumer fails to process a message, the message stays in the queue (or moves to a dead letter queue) and is retried. Without a queue, failed tasks are lost.&lt;/p&gt;

&lt;p&gt;The practical example from TokenPulse: when a user joins the Pro waitlist, the API endpoint puts two tasks in a queue — "send notification email" and "write to Google Sheets." If Google Sheets is temporarily unavailable, the task is retried automatically. The user's HTTP response does not wait for either task to complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load balancing strategies
&lt;/h2&gt;

&lt;p&gt;A load balancer distributes incoming requests across multiple servers. The distribution strategy matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round robin:&lt;/strong&gt; Requests go to servers in order — 1, 2, 3, 1, 2, 3. Simple, works when servers are identical and requests take similar time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least connections:&lt;/strong&gt; Requests go to the server with the fewest active connections. Better when requests have variable processing time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP hash:&lt;/strong&gt; The same client IP always routes to the same server. Useful when you cannot make your application fully stateless and need session affinity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weighted:&lt;/strong&gt; Some servers receive more traffic than others — useful when servers have different capacities.&lt;/p&gt;

&lt;p&gt;For most Next.js applications on Vercel, load balancing is handled automatically. Understanding the strategies matters when you operate your own infrastructure or need to reason about why requests are distributed the way they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The read replica pattern
&lt;/h2&gt;

&lt;p&gt;Write operations go to a primary database. Read operations go to one or more replica databases that are kept in sync with the primary.&lt;/p&gt;

&lt;p&gt;This pattern solves a specific problem: most web applications have dramatically more reads than writes. A read-heavy query that scans a large table on the primary database competes with write operations for I/O. Moving reads to a replica eliminates this contention.&lt;/p&gt;

&lt;p&gt;The trade-off is replication lag — replicas may be slightly behind the primary. For most reads (show me recent posts, display the product catalog), this is acceptable. For reads that immediately follow a write (show me the thing I just created), you need to either read from the primary or implement a mechanism to wait for replication.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to think about trade-offs like a senior engineer
&lt;/h2&gt;

&lt;p&gt;Every system design decision is a trade-off. Senior engineers do not make universally correct decisions — they make decisions appropriate to their specific constraints.&lt;/p&gt;

&lt;p&gt;The questions to ask for any architectural decision:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the actual scale?&lt;/strong&gt; Do not design for 10 million users if you have 100. Design for 10x your current scale and revisit when you get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the cost of being wrong?&lt;/strong&gt; If a caching strategy is wrong, you serve stale data. If a database schema is wrong, you have a migration. Different stakes require different confidence levels before deciding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What can you change later?&lt;/strong&gt; Some decisions are easily reversible — switching a caching library, changing an index. Others are difficult — changing your data model, switching databases. Spend more time on irreversible decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do you actually know?&lt;/strong&gt; Optimize for known bottlenecks, not hypothetical ones. Use monitoring and profiling to find real problems before solving imagined ones.&lt;/p&gt;




&lt;p&gt;If you are building AI-powered applications and want visibility into your token usage costs and rate limits, &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt; is a free Chrome extension that tracks usage across Claude, ChatGPT, Gemini, DeepSeek and Grok — no API key required.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>programming</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned Building a Developer Tool for AI Usage — 6 Weeks of Engineering and Product Lessons</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:15:18 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/what-i-learned-building-a-developer-tool-for-ai-usage-6-weeks-of-engineering-and-product-lessons-3lof</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/what-i-learned-building-a-developer-tool-for-ai-usage-6-weeks-of-engineering-and-product-lessons-3lof</guid>
      <description>&lt;p&gt;Six weeks ago I started building TokenPulse after getting cut off mid-debugging session by Claude's rate limit one too many times. The extension is now live with active users across five platforms. Here is what I actually learned — the engineering decisions, product mistakes, and things I would do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was real but I almost built the wrong solution
&lt;/h2&gt;

&lt;p&gt;My initial instinct was to build a browser extension that scraped Claude's rate limit data and sent it to a backend that users could check via a dashboard. I spent three days on the backend architecture before I stopped and asked the obvious question: why would someone open a separate dashboard to check information they need while they are already inside Claude?&lt;/p&gt;

&lt;p&gt;The right solution was obvious once I asked it: inject the information directly into the page they are already on. A slim bar above the input box. Always visible, never intrusive, no tab switching required.&lt;/p&gt;

&lt;p&gt;The lesson: solve the problem in the context where it actually occurs. Do not add steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  MV3 was harder than the documentation suggested
&lt;/h2&gt;

&lt;p&gt;Chrome's Manifest V3 requirements were the first major technical challenge. The biggest constraint is Content Security Policy — no inline scripts, no &lt;code&gt;eval&lt;/code&gt;, no remotely loaded JavaScript.&lt;/p&gt;

&lt;p&gt;This broke my initial implementation in three ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline event handlers.&lt;/strong&gt; Every &lt;code&gt;onclick="..."&lt;/code&gt; attribute in my HTML failed silently. The fix is &lt;code&gt;addEventListener&lt;/code&gt; for everything, wired after &lt;code&gt;DOMContentLoaded&lt;/code&gt;. Simple in hindsight, took me a day to fully debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service worker lifecycle.&lt;/strong&gt; MV3 background scripts are service workers — Chrome can kill them at any time. Code that assumes the background script is alive between messages will fail intermittently and be almost impossible to reproduce. Everything has to go through &lt;code&gt;chrome.storage.local&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message channel management.&lt;/strong&gt; The &lt;code&gt;chrome.runtime.onMessage&lt;/code&gt; listener has a specific contract: return &lt;code&gt;true&lt;/code&gt; if you are going to call &lt;code&gt;sendResponse&lt;/code&gt; asynchronously, return &lt;code&gt;false&lt;/code&gt; (or nothing) if you are not. Getting this wrong causes the dreaded "message channel closed before response was received" error that appears randomly and is hard to trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dual selector problem
&lt;/h2&gt;

&lt;p&gt;Every platform — Claude, ChatGPT, Gemini, DeepSeek, Grok — updates its frontend regularly. I learned this the hard way when a ChatGPT update broke my content script three days after I published.&lt;/p&gt;

&lt;p&gt;The solution is defensive selector arrays with fallbacks:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INPUT_SELECTORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#prompt-textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// primary&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-id="prompt-textarea"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// fallback 1&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div[contenteditable="true"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// fallback 2&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;selector&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;INPUT_SELECTORS&lt;/span&gt;&lt;span class="p"&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;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never rely on a single selector. Always have at least two fallbacks. Check for null before every DOM operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  I shipped a feature nobody asked for and skipped one everyone wanted
&lt;/h2&gt;

&lt;p&gt;I spent significant time building a detailed token tips panel — a collapsible section in the popup with optimization advice. Nobody uses it. Zero feedback mentions it. It adds visual weight and code complexity for no user value.&lt;/p&gt;

&lt;p&gt;Meanwhile, the most common piece of feedback in the first two weeks was "can you show me how much I've spent this week across all platforms?" — a weekly summary. I had cost tracking per conversation but no weekly rollup.&lt;/p&gt;

&lt;p&gt;The lesson is not "do user research before building" — I had done that. The lesson is that users cannot tell you what they want until they are using the product. The tips panel sounded useful in theory. The weekly summary revealed itself from actual usage patterns.&lt;/p&gt;

&lt;p&gt;Ship the minimum, watch what users do, build what they actually reach for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The notification system needed three rewrites
&lt;/h2&gt;

&lt;p&gt;My first notification implementation fired every time the threshold was crossed, which meant multiple notifications within the same session. Users turned it off immediately.&lt;/p&gt;

&lt;p&gt;My second implementation stored the last notified percentage and only fired when the user crossed a higher threshold. Better — but it never reset, so once you hit 90% and your session reset, you would not get notified at 75% in the next session.&lt;/p&gt;

&lt;p&gt;The third implementation tracks the last notified threshold per window, resets it when usage drops below the threshold, and fires once per crossing per window. This is the version that users actually keep enabled.&lt;/p&gt;

&lt;p&gt;The algorithm sounds simple when written out. Getting there took three iterations and specific user feedback about each failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Vercel over AWS was correct
&lt;/h2&gt;

&lt;p&gt;I spent two days considering hosting options for the marketing site before choosing Vercel. The alternative I seriously considered was AWS — EC2 with nginx and PM2.&lt;/p&gt;

&lt;p&gt;Vercel was correct for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero operational overhead.&lt;/strong&gt; No instance management, no SSL renewal, no process monitoring. Deployment is &lt;code&gt;git push&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The credentials problem doesn't change.&lt;/strong&gt; Whether you deploy on Vercel or EC2, you still need Google Sheets API credentials in environment variables. Switching infrastructure doesn't make that easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue is zero.&lt;/strong&gt; Until the product generates money, infrastructure cost is a constraint. Vercel hobby tier is free. EC2 t3.micro is $8-10/month.
The right time to consider AWS is when you have specific requirements Vercel cannot meet — custom runtime environments, GPU access, VPC networking, specific regional compliance. For a Next.js marketing site with API routes, those requirements do not exist yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Chrome Web Store review process is slower than you expect
&lt;/h2&gt;

&lt;p&gt;First submission: 3 days to review, rejected for missing privacy policy disclosures in the store listing.&lt;/p&gt;

&lt;p&gt;Second submission after fixing: 2 days to review, approved.&lt;/p&gt;

&lt;p&gt;This means plan for a minimum 5-7 day runway between "finished" and "published." If you are building toward a launch date, submit to the Chrome Web Store at least a week before you want to launch.&lt;/p&gt;

&lt;p&gt;Also: the Chrome Web Store's description field is SEO. The keywords in your listing title and description affect where your extension appears in the store's own search. "TokenPulse — Claude Rate Limit &amp;amp; Token Tracker" performs better than "TokenPulse" alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Build the popup last.&lt;/strong&gt; The in-page bar is the core value. I built the popup first because it felt more like a "real" extension. The bar is what users actually use 90% of the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the content scripts with a test harness from day one.&lt;/strong&gt; Testing content scripts means opening the browser, loading the extension, navigating to the target page, and observing behavior. This loop takes 2-3 minutes per test. A mock DOM environment for unit testing would have saved significant time on the notification system rewrites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up error tracking earlier.&lt;/strong&gt; I added error logging after shipping. I had no visibility into content script failures for the first two weeks. Users were silently experiencing broken functionality that I only discovered through direct feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch on one platform, add others after.&lt;/strong&gt; I launched with Claude and ChatGPT support simultaneously. The dual-platform debugging effort at launch was chaotic. Claude alone first, then ChatGPT, would have been cleaner.&lt;/p&gt;




&lt;p&gt;TokenPulse is free and open source at &lt;a href="https://github.com/anu-ship-it/TokenPulse" rel="noopener noreferrer"&gt;github.com/anu-ship-it/TokenPulse&lt;/a&gt;. If you build with AI tools daily and want visibility into your usage, &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;install it here&lt;/a&gt; — works on Claude, ChatGPT, Gemini, DeepSeek and Grok with no API key.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about any of the technical decisions in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building a Full-Stack AI Application with Next.js — Architecture and Implementation</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:13:55 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/building-a-full-stack-ai-application-with-nextjs-architecture-and-implementation-1po8</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/building-a-full-stack-ai-application-with-nextjs-architecture-and-implementation-1po8</guid>
      <description>&lt;p&gt;Building an AI application is easy. Building one that handles streaming responses, manages costs, scales cleanly, and doesn't leak API keys takes more thought. This guide covers the architecture decisions and implementation patterns I have learned from building &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt; and several other AI-powered tools with Next.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next.js 15 (App Router)
TypeScript
Tailwind CSS
Vercel (hosting + edge functions)
Resend (transactional email)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stack handles most AI application requirements without introducing unnecessary complexity. Next.js App Router gives you server components, route handlers, and streaming out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
  layout.tsx          — root layout, metadata, providers
  page.tsx            — landing page
  api/
    chat/route.ts     — AI streaming endpoint
    usage/route.ts    — usage tracking
  dashboard/
    page.tsx          — server component
    client.tsx        — client component with state
lib/
  ai.ts              — AI client configuration
  auth.ts            — session handling
  db.ts              — database client
components/
  chat/
    ChatWindow.tsx    — client component
    Message.tsx       — pure component
    StreamingText.tsx — streaming display
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key architectural decision: keep AI calls in server-side route handlers, never in client components. API keys stay server-side. The client receives streamed responses but never touches credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the AI client
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/ai.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Single instance — reused across requests&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Type-safe model constants&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;smart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-opus-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ModelKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never instantiate the client per-request. The SDK handles connection pooling internally and a single instance is more efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming responses with App Router
&lt;/h2&gt;

&lt;p&gt;Streaming is the single most important UX improvement for AI applications. Without streaming, users stare at a blank screen for 3-10 seconds. With streaming, they see content appear token by token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/chat/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MODELS&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="s1"&gt;@/lib/ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodejs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// required for streaming&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Validate input&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;messages&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Messages required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Create streaming response&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Convert to ReadableStream for the Response&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content_block_delta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text_delta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&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="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&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="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Transfer-Encoding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Content-Type-Options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nosniff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client-side streaming consumption
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/chat/ChatWindow.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ChatWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStreaming&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;abortRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AbortController&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;userMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&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;newMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newMessages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Add empty assistant message to fill in&lt;/span&gt;
    &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt;

    &lt;span class="nx"&gt;abortRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newMessages&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;abortRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request failed&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;// Append to last message&lt;/span&gt;
        &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stream error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;abortRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopStreaming&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;abortRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Input and controls */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rate limiting API routes
&lt;/h2&gt;

&lt;p&gt;Without rate limiting, a single user can exhaust your AI credits in minutes. Implement token bucket rate limiting at the route level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/ratelimit.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;windowMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="na"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;windowMs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your route handler&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&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="s1"&gt;@/lib/ratelimit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rate limit exceeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, use Upstash Redis with their &lt;code&gt;@upstash/ratelimit&lt;/code&gt; package — the in-memory approach above does not work across multiple serverless function instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost tracking per request
&lt;/h2&gt;

&lt;p&gt;Track costs at the API layer so you have data on actual spend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After streaming completes, get usage data&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalMessage&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;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;finalMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Log to your database or analytics&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;logUsage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;pricing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.00&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-opus-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;75.00&lt;/span&gt; &lt;span class="p"&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&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;0&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Environment variables — never expose them
&lt;/h2&gt;

&lt;p&gt;Three rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Never use &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix for AI API keys.&lt;/strong&gt; &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; variables are bundled into the client JavaScript and visible to anyone who opens DevTools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Always validate on startup.&lt;/strong&gt; Add a check in your root layout or a startup file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/env.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for &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;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing required environment variable: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Use &lt;code&gt;.env.example&lt;/code&gt; committed to git, &lt;code&gt;.env.local&lt;/code&gt; never committed.&lt;/strong&gt; Your &lt;code&gt;.gitignore&lt;/code&gt; should always include &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment on Vercel
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;vercel.json&lt;/code&gt; setting that matters for streaming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"functions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"app/api/chat/route.ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"maxDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default Vercel function timeout is 10 seconds on hobby plans — not enough for long AI responses. Set &lt;code&gt;maxDuration&lt;/code&gt; to 60 on pro plans, or implement response chunking to stay within the limit.&lt;/p&gt;

&lt;p&gt;Also add &lt;code&gt;export const runtime = 'nodejs'&lt;/code&gt; to any route that uses streaming. The Edge runtime does not support all Node.js APIs that the AI SDKs depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture decision that matters most
&lt;/h2&gt;

&lt;p&gt;Keep AI logic in server-side route handlers. The temptation is to call AI APIs directly from client components using the AI SDK's client-side helpers. Resist it.&lt;/p&gt;

&lt;p&gt;Server-side AI calls give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API key security&lt;/li&gt;
&lt;li&gt;Cost logging at the server level&lt;/li&gt;
&lt;li&gt;Rate limiting before the request reaches the AI provider&lt;/li&gt;
&lt;li&gt;Error handling in one place
The extra round-trip through your API layer is worth every one of these.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full source code for TokenPulse's website is available at &lt;a href="https://github.com/anu-ship-it/TokenPulse" rel="noopener noreferrer"&gt;github.com/anu-ship-it/TokenPulse&lt;/a&gt;. If you are building AI tools and want to track token usage in real time, &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt; is free and works with no API key.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Context Windows Actually Work in Large Language Models</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:06:58 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/how-context-windows-actually-work-in-large-language-models-23gi</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/how-context-windows-actually-work-in-large-language-models-23gi</guid>
      <description>&lt;p&gt;The context window is the most misunderstood concept in practical LLM usage. Most developers treat it as a hard boundary — "the model can see X tokens" — but the reality is more nuanced and more consequential for how you structure your prompts.&lt;/p&gt;

&lt;p&gt;This is a technical explanation of what context windows actually are, how attention mechanisms work, and why the useful context limit is significantly smaller than the advertised number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a context window contains
&lt;/h2&gt;

&lt;p&gt;Every time you send a message to a language model, the entire conversation history is sent with it. The model does not remember previous messages — it receives them all, in order, as a single input sequence.&lt;/p&gt;

&lt;p&gt;That sequence contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your system prompt or instructions&lt;/li&gt;
&lt;li&gt;Every user message in the conversation&lt;/li&gt;
&lt;li&gt;Every model response in the conversation&lt;/li&gt;
&lt;li&gt;Any documents, code, or files you have pasted&lt;/li&gt;
&lt;li&gt;Tool use results if the model used function calling
All of this is concatenated into a single sequence of tokens and processed together. The context window is the maximum length of this sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Sonnet 4 processes up to 200,000 tokens at once. GPT-4o handles 128,000. Gemini 1.5 Pro handles 1,000,000. These numbers sound enormous until you realize that a single verbose debugging session can consume 30,000-50,000 tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  How attention works — the mechanism behind context
&lt;/h2&gt;

&lt;p&gt;To understand why context windows behave the way they do, you need to understand attention — specifically, self-attention in transformers.&lt;/p&gt;

&lt;p&gt;When a model processes your input, every token attends to every other token in the sequence. The attention score between two tokens determines how much the model's representation of token A is influenced by token B.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attention_score(query_A, key_B) = dot_product(Q_A, K_B) / sqrt(d_k)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;Q_A&lt;/code&gt; is the query vector for token A and &lt;code&gt;K_B&lt;/code&gt; is the key vector for token B. Higher scores mean more influence.&lt;/p&gt;

&lt;p&gt;The critical insight: &lt;strong&gt;attention is computed across the full context window, but the scores are not uniform&lt;/strong&gt;. Recent tokens tend to have higher attention scores because the model has been trained on sequences where recent context is more relevant. Earlier tokens receive lower attention scores and contribute less to the model's representations.&lt;/p&gt;

&lt;p&gt;This is not a bug — it is the correct inductive bias for most language tasks. In normal text, what just happened is more relevant than what happened 10,000 tokens ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why quality degrades before the hard limit
&lt;/h2&gt;

&lt;p&gt;The practical consequence of attention distribution is that context quality is not binary. It does not work at 100% until 200,000 tokens and then fail. It degrades gradually as the context grows.&lt;/p&gt;

&lt;p&gt;The degradation pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0-40% capacity:&lt;/strong&gt; Full quality. The model attends effectively to everything in the context. Constraints established early are respected. Earlier decisions are remembered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;40-60% capacity:&lt;/strong&gt; Slight degradation. The model may occasionally miss a constraint from the beginning of the conversation. Earlier architectural decisions get less weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;60-80% capacity:&lt;/strong&gt; Noticeable degradation. Constraints from early in long conversations get ignored. The model may contradict earlier decisions. Code quality tends to decrease.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;80-100% capacity:&lt;/strong&gt; Significant degradation. The model effectively operates on a shorter effective context than the full window. Early content is present but not meaningfully attended to.&lt;/p&gt;

&lt;p&gt;The practical takeaway: &lt;strong&gt;the useful context limit for complex tasks is roughly 60% of the advertised context window.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The "lost in the middle" problem
&lt;/h2&gt;

&lt;p&gt;Research on long-context LLMs has identified a specific failure mode called "lost in the middle" — models perform well on information at the beginning and end of long contexts, but poorly on information in the middle.&lt;/p&gt;

&lt;p&gt;This has a direct implication for how you structure long prompts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Less effective:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[System instructions - beginning]
[Background context - middle, long]
[Specific question - end]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;More effective:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[System instructions - beginning]
[Specific question - near beginning]
[Relevant background context - end]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Information you want the model to attend to most strongly should be either at the very beginning or the very end of the context — not buried in the middle of a long conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical implications for developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Restart conversations deliberately
&lt;/h3&gt;

&lt;p&gt;The most effective strategy for long-running tasks is to restart conversations when the context reaches 60% capacity.&lt;/p&gt;

&lt;p&gt;Ask for a summary before restarting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Summarize the key decisions, constraints, current state, and 
any important code patterns established in this conversation. 
Keep it under 300 words.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a new conversation with that summary. You preserve everything that matters and reset the context overhead. The summary typically condenses 30,000-50,000 tokens of conversation into 1,000-2,000 tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Front-load the constraints that matter most
&lt;/h3&gt;

&lt;p&gt;If you have critical constraints — "never use class components," "all functions must be async," "this code runs on Node 18" — put them at the beginning of the system prompt AND repeat them at the end of your most recent message.&lt;/p&gt;

&lt;p&gt;The repetition sounds redundant but is effective: the constraint appears at two positions that both receive strong attention — the beginning and the most recent content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use separate conversations for separate problems
&lt;/h3&gt;

&lt;p&gt;Every unrelated message adds tokens without adding relevant context. If you are debugging three separate functions in a single conversation, each exchange about function A is reducing the effective context available for function C.&lt;/p&gt;

&lt;p&gt;One problem per conversation is more token-efficient and produces higher quality responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor context usage in real time
&lt;/h3&gt;

&lt;p&gt;Most platforms hide context window usage from users. &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt; injects a live bar above the input box on Claude, ChatGPT, Gemini, DeepSeek and Grok showing your current context window percentage. When it hits 60%, that is the signal to summarize and restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 1M token context window — does it actually help?
&lt;/h2&gt;

&lt;p&gt;Gemini 1.5 Pro's 1,000,000 token context window is a genuine technical achievement. But the attention degradation problem scales with context length — a 1M token context window does not give you 5x the effective context of a 200k window.&lt;/p&gt;

&lt;p&gt;For tasks where you need to search across a very large codebase or document set, large context windows genuinely help. For typical development tasks — debugging, code review, architecture discussion — the effective range of 200k is more than sufficient when used well.&lt;/p&gt;

&lt;p&gt;The practical benchmark: if you are regularly hitting context limits on Claude or GPT-4o, the problem is almost always conversation management, not context window size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Context windows contain the entire conversation history sent on every request&lt;/li&gt;
&lt;li&gt;Attention mechanisms weight recent tokens more heavily than earlier ones&lt;/li&gt;
&lt;li&gt;Quality degrades noticeably at 60-70% capacity, not just at the hard limit&lt;/li&gt;
&lt;li&gt;Information in the middle of long contexts gets less attention than beginning and end&lt;/li&gt;
&lt;li&gt;Restart at 60% with a summary, not when the model tells you the conversation is too long&lt;/li&gt;
&lt;li&gt;One problem per conversation is more efficient than long multi-topic sessions&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>What Are AI Tokens and Why Should Developers Care?</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 15:04:31 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/what-are-ai-tokens-and-why-should-developers-care-5c99</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/what-are-ai-tokens-and-why-should-developers-care-5c99</guid>
      <description>&lt;p&gt;If you have used Claude, ChatGPT, or any large language model API, you have encountered tokens. They show up in pricing pages, rate limit errors, and API documentation. But most explanations of tokens are either too vague ("roughly 4 characters") or too academic (subword tokenization, BPE encoding). &lt;/p&gt;

&lt;p&gt;This article explains tokens at the level a developer actually needs — what they are, why they matter for the things you care about, and how to think about them when building with AI APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a token actually is
&lt;/h2&gt;

&lt;p&gt;A token is the basic unit of text that a language model processes. It is not a character, and it is not a word — it sits somewhere in between.&lt;/p&gt;

&lt;p&gt;Modern language models use a technique called Byte Pair Encoding (BPE) to split text into tokens. The tokenizer learns which character sequences appear frequently together and groups them into single tokens. Common words become single tokens. Rare words get split into multiple tokens. Punctuation, spaces, and special characters all become tokens too.&lt;/p&gt;

&lt;p&gt;Some examples using GPT-4's tokenizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"hello"        → 1 token
"tokenization" → 3 tokens  (token + ization split further)
"ChatGPT"      → 3 tokens
" the"         → 1 token   (space included)
"2026"         → 1 token
"\n"           → 1 token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 4-characters-per-token approximation works because English text averages roughly 4 characters per token across typical writing. Code, which has more symbols and shorter identifiers, tends to be closer to 3 characters per token. Dense technical writing can be 5 or more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tokens matter for developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Cost
&lt;/h3&gt;

&lt;p&gt;Every major AI API charges by token. Claude Sonnet 4 costs $3.00 per million input tokens and $15.00 per million output tokens. GPT-4o costs $2.50 per million input and $10.00 per million output.&lt;/p&gt;

&lt;p&gt;A typical back-and-forth debugging session might consume 10,000-50,000 tokens total. At Claude Sonnet pricing, that is $0.03 to $0.15 per session — which sounds trivial until you have a team of 10 developers each running 5 sessions per day.&lt;/p&gt;

&lt;p&gt;The output token cost matters more than most developers realize. When you ask Claude to write a 500-line function, you are paying 5x more per token for the output than the input. Prompts that ask for verbose explanations cost significantly more than prompts that ask for concise code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Context window limits
&lt;/h3&gt;

&lt;p&gt;Every model has a context window — the maximum number of tokens it can process in a single request, including both input and output. Claude Sonnet 4 has a 200,000 token context window. GPT-4o has 128,000. Gemini 1.5 Pro has 1,000,000.&lt;/p&gt;

&lt;p&gt;This means a 200,000 token context window is approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;800,000 characters of text&lt;/li&gt;
&lt;li&gt;150,000 words&lt;/li&gt;
&lt;li&gt;About 600 pages of a typical book&lt;/li&gt;
&lt;li&gt;A large codebase (though not an entire monorepo)
In practice, you hit the context limit much faster than these numbers suggest because:&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Your system prompt consumes tokens&lt;/li&gt;
&lt;li&gt;Every message in the conversation accumulates&lt;/li&gt;
&lt;li&gt;Every response the model generates adds to the total&lt;/li&gt;
&lt;li&gt;Large code pastes consume tokens proportional to their length
A single paste of a 500-line file is roughly 25,000-30,000 tokens — 12-15% of Claude's context window in one shot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Rate limits
&lt;/h3&gt;

&lt;p&gt;AI providers enforce rate limits in tokens per unit time, not requests per unit time. Claude has a 5-hour session limit and a 7-day weekly limit, both measured in token consumption. OpenAI's API has tokens-per-minute (TPM) and tokens-per-day (TPD) limits.&lt;/p&gt;

&lt;p&gt;This means two things developers often get wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message count is not what matters.&lt;/strong&gt; Sending 100 short messages consumes far fewer tokens than sending 10 messages with large code pastes. Rate limits are about total token consumption, not request frequency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output tokens count too.&lt;/strong&gt; If you ask a model for a very long, detailed response, you are consuming output tokens against your rate limit just as much as the input.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to estimate tokens without calling the API
&lt;/h2&gt;

&lt;p&gt;For quick estimates, the 4-characters-per-token approximation is sufficient:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateTokens&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Examples:&lt;/span&gt;
&lt;span class="nf"&gt;estimateTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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;// → 4&lt;/span&gt;
&lt;span class="nf"&gt;estimateTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function authenticate(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// → 6&lt;/span&gt;
&lt;span class="nf"&gt;estimateTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeCodeFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// → roughly accurate ±10%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For precise counts, use the official tokenizers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI:&lt;/strong&gt; &lt;code&gt;tiktoken&lt;/code&gt; library (Python and JavaScript)&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;encoding_for_model&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="s1"&gt;tiktoken&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;encoding_for_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Anthropic:&lt;/strong&gt; Claude's API has a &lt;code&gt;count_tokens&lt;/code&gt; endpoint:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For browser extensions and client-side tools that cannot call the API, client-side estimation with the ±8% approximation is the practical approach — it is accurate enough for showing users whether they are at 20% or 80% of their context window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical token optimization for developers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Be specific, not verbose.&lt;/strong&gt; "Refactor this function to handle null values" consumes far fewer tokens than a paragraph explaining the same thing. Models understand concise instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paste targeted context, not entire files.&lt;/strong&gt; Instead of pasting a 500-line file, paste the specific function plus 10-15 lines of surrounding context. You get equally good answers and use a fraction of the tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summarize long conversations.&lt;/strong&gt; After a long debugging session, ask the model to summarize key decisions and findings in under 200 words, start a new conversation with that summary, and continue. You preserve context that matters and reset token overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use faster models for iteration.&lt;/strong&gt; If you are iterating on boilerplate, formatting, or simple transformations, use Haiku or GPT-4o mini. They consume the same rate limit quota but cost 10-20x less per token. Reserve the premium models for complex reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor your actual usage.&lt;/strong&gt; Most developers significantly underestimate their token consumption because the interface hides it. Tools like &lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt; show real-time token counts, context window percentage, and estimated cost directly in the browser — no API key required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key numbers to remember
&lt;/h2&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;Context Window&lt;/th&gt;
&lt;th&gt;Input Cost / 1M&lt;/th&gt;
&lt;th&gt;Output Cost / 1M&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4&lt;/td&gt;
&lt;td&gt;200k tokens&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4&lt;/td&gt;
&lt;td&gt;200k tokens&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;td&gt;$75.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;128k tokens&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 2.0 Flash&lt;/td&gt;
&lt;td&gt;1M tokens&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V3&lt;/td&gt;
&lt;td&gt;128k tokens&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;td&gt;$1.10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Understanding tokens is the foundation of working effectively with AI APIs. Once you internalize that everything is measured in tokens — cost, limits, context — the behavior of these systems becomes much more predictable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built a Chrome Extension to Track AI Token Usage — Here's How It Works</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 12:49:04 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/i-built-a-chrome-extension-to-track-ai-token-usage-heres-how-it-works-1701</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/i-built-a-chrome-extension-to-track-ai-token-usage-heres-how-it-works-1701</guid>
      <description>&lt;p&gt;Six weeks ago I got cut off mid-debugging session by Claude's rate limit with no warning. Two hours of context gone. I started looking for a tool that would show me how close I was before it happened. Nothing existed that worked across more than one platform without requiring an API key.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;TokenPulse&lt;/a&gt; is a Chrome extension (MV3) that injects a live token bar above the input box on Claude, ChatGPT, Gemini, DeepSeek and Grok. It tracks context window usage, rate limits, cost estimates, and daily history — all from your existing browser session, no API key required.&lt;/p&gt;

&lt;p&gt;Here's how it works technically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Scripts (per platform)
        ↓
Background Service Worker
        ↓
Chrome Storage API (local)
        ↓
Popup UI
        ↓
Desktop Notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension runs a content script on each supported domain. Each script is responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reading token usage data from that platform&lt;/li&gt;
&lt;li&gt;Injecting the visual bar above the input box&lt;/li&gt;
&lt;li&gt;Sending data to the background service worker via &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt;
The service worker aggregates data, writes to &lt;code&gt;chrome.storage.local&lt;/code&gt;, checks notification thresholds, and serves data to the popup on demand.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Claude's rate limits are read
&lt;/h2&gt;

&lt;p&gt;Claude is the only platform that exposes real rate limit data through its internal API. When you use claude.ai, the browser session makes requests to a usage endpoint that returns exact utilization percentages and reset timestamps.&lt;/p&gt;

&lt;p&gt;The content script intercepts this data by hooking into the platform's network requests using a &lt;code&gt;MutationObserver&lt;/code&gt; to detect when Claude updates its state, then reading the cached response.&lt;/p&gt;

&lt;p&gt;The response looks roughly like:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;five_hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;utilization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reset_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-15T14:14:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;seven_day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;utilization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;reset_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-21T21:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives exact percentages — not estimates. The popup shows these directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-side token estimation for other platforms
&lt;/h2&gt;

&lt;p&gt;ChatGPT, Gemini, DeepSeek and Grok don't expose usage data the same way. For these, TokenPulse estimates token usage from the conversation DOM.&lt;/p&gt;

&lt;p&gt;The estimation approach:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateTokens&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ~4 characters per token — standard approximation&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getConversationTokens&lt;/span&gt;&lt;span class="p"&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;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-message-author-role]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;estimateTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accuracy is approximately ±8% — sufficient for knowing whether you're at 20% or 80% of your context window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting the token bar
&lt;/h2&gt;

&lt;p&gt;The bar injection uses a &lt;code&gt;MutationObserver&lt;/code&gt; to watch for the input box appearing in the DOM, then inserts a container element immediately above it.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;inputBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PLATFORM_INPUT_SELECTOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputBox&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tp-bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;injectBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputBox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bar is a thin div with a gradient fill that updates via CSS transition whenever new token data arrives:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pct&lt;/span&gt;&lt;span class="p"&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;fill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tp-bar-fill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSS transition handles the smooth animation — no JavaScript animation loops needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost estimation
&lt;/h2&gt;

&lt;p&gt;Cost is estimated by multiplying token count by current model pricing:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PRICING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// per 1M tokens&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-opus-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;75.00&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;10.00&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-2.0-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.40&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deepseek-v3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&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;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PRICING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;price&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;0&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Model detection reads the platform's model selector from the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The notification system
&lt;/h2&gt;

&lt;p&gt;Notifications fire at configurable thresholds (default: 75%, 90%, 100%) using Chrome's notifications API. The threshold system is designed to fire once per crossing and reset when usage drops:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldNotify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&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;thresholds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`notify_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;crossed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currentPct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastNotified&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;getLastNotified&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;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastNotified&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crossed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;crossed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="nx"&gt;lastNotified&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crossed&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveLastNotified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastNotified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crossed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents notification spam — you get one notification when you cross 75%, another when you cross 90%, and they reset when your usage drops back below the threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  MV3 service worker constraints
&lt;/h2&gt;

&lt;p&gt;Chrome's Manifest V3 requirement means the background script is a service worker — not a persistent background page. Service workers can be killed by Chrome at any time, which creates two constraints:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No persistent state in memory.&lt;/strong&gt; Everything goes through &lt;code&gt;chrome.storage.local&lt;/code&gt;. The service worker reads from storage on every message handler invocation rather than keeping state in variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async message handling.&lt;/strong&gt; The &lt;code&gt;chrome.runtime.onMessage&lt;/code&gt; listener must handle the async/sync distinction carefully. Fire-and-forget messages (like saving usage data) return &lt;code&gt;false&lt;/code&gt; immediately. Messages that need a response return &lt;code&gt;true&lt;/code&gt; and call &lt;code&gt;sendResponse&lt;/code&gt; after the async work completes.&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="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SAVE_USAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fire and forget — no response needed&lt;/span&gt;
    &lt;span class="nx"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET_ALL_DATA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Async response needed&lt;/span&gt;
    &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUsage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHistory&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSettings&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// keep channel open&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;MV3 CSP is strict.&lt;/strong&gt; No inline scripts, no &lt;code&gt;eval&lt;/code&gt;, no remote code execution. Every event handler must be attached via &lt;code&gt;addEventListener&lt;/code&gt; — no &lt;code&gt;onclick&lt;/code&gt; attributes. This caught me early and took a full debugging session to unpack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform DOM structures change without notice.&lt;/strong&gt; Claude, ChatGPT and Gemini update their frontends regularly. Selectors that work today break in a week. The solution is multiple fallback selectors and defensive &lt;code&gt;?.&lt;/code&gt; access throughout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service worker timing is unpredictable.&lt;/strong&gt; Chrome can kill and restart the service worker between messages. Code that assumes the worker is alive from a previous message will fail intermittently and be very hard to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local storage is fast enough.&lt;/strong&gt; I was worried &lt;code&gt;chrome.storage.local&lt;/code&gt; would be too slow for real-time updates. In practice, reads complete in under 5ms and the popup feels instant.&lt;/p&gt;

&lt;p&gt;The extension is open source at &lt;a href="https://github.com/anu-ship-it/TokenPulse" rel="noopener noreferrer"&gt;github.com/anu-ship-it/TokenPulse&lt;/a&gt;. If you're building a Chrome extension that reads from AI platforms, feel free to look at how the content scripts are structured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://token-pulse.in" rel="noopener noreferrer"&gt;Install TokenPulse free&lt;/a&gt; — works on Claude, ChatGPT, Gemini, DeepSeek and Grok with no API key.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>extensions</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How Claude's rate limits actually work — and how I track them in real time</title>
      <dc:creator>Anoop Kumar</dc:creator>
      <pubDate>Sun, 09 Aug 2026 22:09:34 +0000</pubDate>
      <link>https://dev.to/anoop_kumar_63925e275ea06/how-claudes-rate-limits-actually-work-and-how-i-track-them-in-real-time-9da</link>
      <guid>https://dev.to/anoop_kumar_63925e275ea06/how-claudes-rate-limits-actually-work-and-how-i-track-them-in-real-time-9da</guid>
      <description>&lt;p&gt;I was two hours into a debugging session with Claude when it just stopped.&lt;/p&gt;

&lt;p&gt;No warning. No countdown. No indication I was close. Just a message telling me I'd reached my usage limit.&lt;/p&gt;

&lt;p&gt;Two hours of context — gone. I had to start over.&lt;/p&gt;

&lt;p&gt;That was the moment I started actually trying to understand how Claude's rate limits work. What I found surprised me, and I haven't seen it explained clearly anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude has two completely separate rate limits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most developers assume there's one limit. There are two, and they operate independently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 5-hour session limit&lt;/strong&gt; tracks your message volume within any rolling 5-hour window. This is the one that catches most developers off guard because it resets on a rolling basis — not at a fixed time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 7-day weekly limit&lt;/strong&gt; tracks cumulative usage across a rolling 7-day period. This one resets 7 days after your first message in the window — not on Sunday, not at midnight.&lt;/p&gt;

&lt;p&gt;You can be at 0% on the weekly limit and 90% on the session limit. You can max both on the same day if you work in concentrated bursts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why "rolling" matters more than you think&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rolling reset is the part that trips people up most.&lt;/p&gt;

&lt;p&gt;If you sent your first message at 9:14am on Tuesday, your 5-hour window resets at 2:14pm — not at 10am, not at noon, not at midnight. If you send your first message on Monday at 11pm, your 7-day limit resets the following Monday at 11pm.&lt;/p&gt;

&lt;p&gt;This means the mental model of "it resets Sunday night" or "it resets every morning" is wrong for most users. The reset time is personal to your usage pattern.&lt;/p&gt;

&lt;p&gt;The practical consequence: you can't plan around a fixed reset schedule. You need to know the actual countdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Claude exposes through its internal API&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part most developers don't know exists.&lt;/p&gt;

&lt;p&gt;When you use Claude through the browser at claude.ai, the interface makes requests to an internal usage endpoint that returns your actual utilization data — not estimates, not approximations, but the exact numbers Claude uses to decide when to cut you off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"five_hour"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"utilization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reset_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-15T14:14:00Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seven_day"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"utilization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reset_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-21T21:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;utilization&lt;/code&gt; field is a percentage — 0.82 means you're at 82% of your 5-hour limit. The &lt;code&gt;reset_at&lt;/code&gt; field is the exact UTC timestamp when that window resets.&lt;/p&gt;

&lt;p&gt;This data is available to anyone using Claude through the browser. You don't need an API key. You don't need special access. Your existing browser session already has permission to read it — because Claude itself uses it to show rate limit warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Claude's rate limits are measured in tokens, not messages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The limits aren't per-message — they're per-token. A short message consumes far fewer tokens than a long one with a code paste.&lt;/p&gt;

&lt;p&gt;A 500-line file is approximately 25,000-30,000 tokens. A typical detailed response might be 1,000-2,000 tokens. A debugging session where you paste large code snippets can burn through your 5-hour window in under an hour.&lt;/p&gt;

&lt;p&gt;The models also matter. Claude Opus is significantly more expensive per token than Claude Sonnet or Haiku, which affects how quickly you consume your quota.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The degradation problem — quality drops before the limit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's something that isn't documented anywhere: Claude's response quality degrades before you hit the hard limit.&lt;/p&gt;

&lt;p&gt;The mechanism is attention. Large language models weight recent tokens more heavily than distant ones. In a very long conversation, Claude can technically "see" everything you've written, but its effective attention to content from early in the conversation weakens as more content is added.&lt;/p&gt;

&lt;p&gt;Practically, this means you'll notice Claude starting to ignore constraints or forget decisions established earlier in the conversation — usually around 60-70% of the context window. By the time you're at 80%, you're often getting meaningfully worse answers than you would in a fresh conversation with a good summary.&lt;/p&gt;

&lt;p&gt;The right time to restart is at 60% of the context window — not when Claude tells you the conversation is too long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I track this in real time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After getting cut off enough times, I built a Chrome extension that reads this data directly from Claude's internal API and shows it in the browser.&lt;/p&gt;

&lt;p&gt;TokenPulse injects a slim bar above Claude's input box showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. Context window percentage in real time&lt;/li&gt;
&lt;li&gt;2. 5-hour session utilization (exact percentage from Claude's API)&lt;/li&gt;
&lt;li&gt;3. 7-day weekly utilization (same)&lt;/li&gt;
&lt;li&gt;4. Countdown to each reset&lt;/li&gt;
&lt;li&gt;5. Estimated cost per conversation, per day, per week&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also works on ChatGPT, Gemini, DeepSeek and Grok — though those platforms don't expose rate limit data the same way Claude does, so those use client-side estimation.&lt;/p&gt;

&lt;p&gt;No API key required. No account. It reads your existing browser session — the same one Claude already uses to pull this data for its own interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical strategies based on this understanding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check both limits before starting heavy sessions&lt;/strong&gt;. If you're at 70% of your 5-hour window, either work quickly or wait for the reset. Starting a 2-hour debugging session at 70% almost guarantees getting cut off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start new conversations for each distinct problem&lt;/strong&gt;. Every message accumulates context. If you're debugging three separate functions, three conversations is more efficient than one long one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restart at 60% context, not when Claude tells you to&lt;/strong&gt;. By the time Claude warns you, quality has already degraded. Summarize at 60%, start fresh, paste the summary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Haiku and Sonnet for iteration, Opus for final decisions&lt;/strong&gt;. All models consume the same rate limit quota. Matching model capability to task complexity extends how long you can work before hitting limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time heavy sessions around your actual reset time&lt;/strong&gt;. Check your reset countdown before starting an intensive session. If your 5-hour window resets in 20 minutes, waiting is often worth it.&lt;/p&gt;

&lt;p&gt;The extension is free and open source if you want to look at how it reads Claude's usage data:&lt;/p&gt;

&lt;p&gt;Chrome Web Store: &lt;a href="https://www.token-pulse.in/" rel="noopener noreferrer"&gt;token-pulse.in&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/anu-ship-it/TokenPulse" rel="noopener noreferrer"&gt;github.com/anu-ship-it/TokenPulse&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
Happy to answer questions about how the rate limit detection works in the comments.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>developer</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
