<?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: Meterbase</title>
    <description>The latest articles on DEV Community by Meterbase (@usemeterbase).</description>
    <link>https://dev.to/usemeterbase</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%2F4159432%2Fb6e1add7-33db-4702-a706-dd80b02f36f7.jpg</url>
      <title>DEV Community: Meterbase</title>
      <link>https://dev.to/usemeterbase</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usemeterbase"/>
    <language>en</language>
    <item>
      <title>Soft cap or hard cap? Deciding what happens when a customer hits their limit</title>
      <dc:creator>Meterbase</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:22:04 +0000</pubDate>
      <link>https://dev.to/meterbase/soft-cap-or-hard-cap-deciding-what-happens-when-a-customer-hits-their-limit-1d3f</link>
      <guid>https://dev.to/meterbase/soft-cap-or-hard-cap-deciding-what-happens-when-a-customer-hits-their-limit-1d3f</guid>
      <description>&lt;p&gt;Sooner or later, a paying customer hits the edge of their plan in the middle of something they care about. What your product does in that moment is a pricing decision, a product decision and an engineering decision at once. Most teams make it by accident, in whichever code path ran out of credits first.&lt;/p&gt;

&lt;p&gt;Here are the options, and what each one needs underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Hard cap
&lt;/h2&gt;

&lt;p&gt;The customer gets exactly what the plan includes. When it's gone, the next request is refused until the period renews or they buy more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; prepaid credits, free tiers, and products where each unit is expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; refusing work halfway through a multi-step job. If an agent run needs ten steps and the balance covers seven, a hard cap mid-run leaves the customer with a broken result. Gate the whole job up front, with a hold sized to the estimate, rather than step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Soft cap with overage
&lt;/h2&gt;

&lt;p&gt;The plan includes an amount. Usage past it keeps working and is billed as overage when the period ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; business customers who'd rather pay more than be interrupted, and products where a usage spike means things are going well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; surprise invoices. A soft cap without alerts is how you get an angry email from someone's finance team. You also need an exact, closed number for included usage and overage per customer per period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: No cap
&lt;/h2&gt;

&lt;p&gt;Nothing is included or limited. Everything is metered and billed afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; pay-as-you-go API products sold to developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; runaway scripts. With no cap, rate limits are the only thing between a bug in a customer's loop and a very large bill. Keep per-customer rate limits even when you don't cap totals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 4: Mix them per meter
&lt;/h2&gt;

&lt;p&gt;Real plans usually combine them: unlimited chat messages, 5,000 image generations a month with overage, and a hard cap on the most expensive thing you sell. A plan is a set of decisions, one per thing you measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whatever you pick, add these two
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Thresholds.&lt;/strong&gt; Tell customers at 80%, 90% and 100%, once each per period. That turns a hard stop into a top-up, and an overage invoice into something they saw coming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-ups.&lt;/strong&gt; For hard caps, give customers a way to buy more right now. A top-up should have its own expiry, separate from the monthly allowance, so it doesn't vanish at renewal.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this maps onto Meterbase
&lt;/h2&gt;

&lt;p&gt;In Meterbase, each plan allowance is one of these, set per meter, so one plan can mix them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a finite amount (hard cap)&lt;/li&gt;
&lt;li&gt;a finite amount with &lt;code&gt;allow_overage: true&lt;/code&gt; (soft cap)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no_cap&lt;/code&gt; (no cap)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your code doesn't change between them. You still &lt;code&gt;check&lt;/code&gt; before the work and &lt;code&gt;track&lt;/code&gt; after. Under a soft cap, &lt;code&gt;check&lt;/code&gt; keeps allowing past the included amount and tells you so:&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;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"no_cap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allow_overage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate_limits"&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;Threshold crossings become webhooks, rate limits apply per customer either way, and when a period closes you read its exact usage and overage to bill from.&lt;/p&gt;

&lt;p&gt;If you're making this decision for your own product right now, we're happy to talk it through: &lt;a href="https://meterbase.tech" rel="noopener noreferrer"&gt;meterbase.tech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>startup</category>
      <category>ai</category>
      <category>api</category>
    </item>
    <item>
      <title>Two requests, one last credit: the race condition in usage-based billing</title>
      <dc:creator>Meterbase</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:21:47 +0000</pubDate>
      <link>https://dev.to/meterbase/two-requests-one-last-credit-the-race-condition-in-usage-based-billing-23h3</link>
      <guid>https://dev.to/meterbase/two-requests-one-last-credit-the-race-condition-in-usage-based-billing-23h3</guid>
      <description>&lt;p&gt;Here's the most common way to gate paid work:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credits&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;getCredits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 10 left&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;credits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cost&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="s2"&gt;Out of credits&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;result&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;runJob&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;span class="c1"&gt;// takes 20 seconds&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deductCredits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads fine and works in testing. In production, a customer opens two tabs, or an agent fires five tool calls in parallel, and every request reads the same balance before any of them writes. They all pass. The customer ends the day with a negative balance, and you paid for the compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a transaction doesn't fix it
&lt;/h2&gt;

&lt;p&gt;Wrapping the read and the deduct in a transaction doesn't help when the job runs in between, because you can't hold a database transaction open for 20 seconds while a GPU works. And if you deduct up front, you still have to handle jobs that cost less than you estimated, jobs that fail, and workers that crash before refunding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: make check-and-deduct atomic
&lt;/h2&gt;

&lt;p&gt;The first fix is to let the database do the comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no row comes back, the customer couldn't afford it. Two concurrent requests can't both take the last credits, because the second update sees the first one's write.&lt;/p&gt;

&lt;p&gt;That works when you know the cost up front. For AI work you usually don't: a reply's token count, a render's length or a scrape's page count is only known after the job finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: hold an estimate, settle the actual
&lt;/h2&gt;

&lt;p&gt;The pattern that handles unknown costs is a hold, like a card hold at a hotel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before the work, atomically set aside an estimate. The customer's available balance drops by that amount, so a parallel request can't use it.&lt;/li&gt;
&lt;li&gt;Run the work.&lt;/li&gt;
&lt;li&gt;Commit the real cost. Whatever's left of the hold goes back.&lt;/li&gt;
&lt;li&gt;If the work fails, release the hold and record nothing.&lt;/li&gt;
&lt;li&gt;If the process dies and nobody commits or releases, the hold expires on its own and the capacity comes back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step is the one teams forget. Without expiry, every crashed worker permanently eats part of a customer's balance.&lt;/p&gt;

&lt;p&gt;A minimal holds table looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;holds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;quantity&lt;/span&gt;    &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;closed_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Available" then becomes the allowance, minus usage, plus unspent top-ups, minus open holds that haven't expired. Every reserve has to compute that and insert the hold atomically for that customer, which in practice means a row lock or serializable isolation per customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: decide which work needs a hold
&lt;/h2&gt;

&lt;p&gt;Holds cost a write and make concurrent requests for the same customer wait their turn. Not everything needs one. A useful rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cheap work you can afford to overshoot slightly&lt;/strong&gt;, like a chat reply against a million-token allowance: check, run, record. Two replies racing past the check overshoot by a few thousand tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expensive work you can't afford to do twice&lt;/strong&gt;, like image batches, video renders or long agent runs: hold, run, then commit or release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this looks like in Meterbase
&lt;/h2&gt;

&lt;p&gt;We built Meterbase so this is a couple of calls. For expensive work:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hold&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;meterbase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acct_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image_generations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expires_in_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// longer than the work takes&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;hold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&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;`Refused: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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;images&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;generateImages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;await&lt;/span&gt; &lt;span class="nx"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// the work failed: record nothing&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&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="c1"&gt;// the rest of the hold comes back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two concurrent reserves can't be granted the same capacity, and a hold nobody closes expires after &lt;code&gt;expires_in_seconds&lt;/code&gt; (1 to 900, default 60). For cheap work you'd use &lt;code&gt;check&lt;/code&gt; and &lt;code&gt;track&lt;/code&gt; instead, which hold nothing.&lt;/p&gt;

&lt;p&gt;If you're building this yourself, the SQL above is a solid start. If you'd rather not own it, take a look at &lt;a href="https://meterbase.tech" rel="noopener noreferrer"&gt;meterbase.tech&lt;/a&gt;. We're happy to help you set it up.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>database</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The parts of a credit system nobody puts in the estimate</title>
      <dc:creator>Meterbase</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:21:17 +0000</pubDate>
      <link>https://dev.to/meterbase/the-parts-of-a-credit-system-nobody-puts-in-the-estimate-3063</link>
      <guid>https://dev.to/meterbase/the-parts-of-a-credit-system-nobody-puts-in-the-estimate-3063</guid>
      <description>&lt;p&gt;If your product sells credits, tokens, minutes or API calls, someone on your team has probably said "we'll just add a &lt;code&gt;credits&lt;/code&gt; column to the users table." It's a reasonable first move. It's also where most teams start building a billing system they never planned to own.&lt;/p&gt;

&lt;p&gt;Here's what that column turns into once real customers arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The balance isn't one number
&lt;/h2&gt;

&lt;p&gt;A customer on a monthly plan has an allowance that resets. Then they buy a top-up pack, which shouldn't reset. Then you give them a signup bonus that expires in 30 days. Now "how many credits do they have?" is a sum over several buckets, each with its own rules, and you have to decide which bucket to spend first. Usually it's the one that expires soonest.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "Monthly" means each customer's month
&lt;/h2&gt;

&lt;p&gt;Allowances renew on the customer's billing date, not on the 1st. A customer who signed up on the 17th renews on the 17th. A customer who upgraded on the 9th might renew on the 9th from now on, or keep the 17th, depending on how you prorate. Your renewal job now needs to know every customer's anchor date.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Plan changes mid-period
&lt;/h2&gt;

&lt;p&gt;Upgrade today, and what happens to the allowance? Do they get the full new amount now, a prorated share, or the new amount at the next renewal? Downgrades are worse: what if they've already used more than the smaller plan includes? Whatever you choose, you need a history of which plan applied when, or you can't explain an invoice later.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Two requests, one last credit
&lt;/h2&gt;

&lt;p&gt;Your app checks the balance, runs the job, then deducts. With parallel requests, two jobs can both see "10 credits left", both run, and both deduct. The customer ends up at −10. For cheap work that's fine. For a video render or a batch of image generations, it's real money. Fixing it means holding capacity before the work starts and settling after.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Failed jobs
&lt;/h2&gt;

&lt;p&gt;If you hold or deduct credits before a job and the job fails, the customer should get them back. If your worker crashes mid-job, nobody calls the refund. So holds need an expiry, and something has to clean them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Bursts
&lt;/h2&gt;

&lt;p&gt;A monthly allowance doesn't stop one customer's runaway script from spending the whole month in ten minutes. You need rate limits per customer as well as totals, often in more than one window, such as per minute and per hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Telling customers before they run out
&lt;/h2&gt;

&lt;p&gt;Customers would rather top up at 80% than find out they're at 100% when a job fails. That means tracking thresholds per customer per period and notifying once, not on every request after the line is crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Usage for billing
&lt;/h2&gt;

&lt;p&gt;At the end of a period, finance wants an exact number: how much this customer used, how much was included, and how much was overage. That number has to stay put once the period closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it adds up to
&lt;/h2&gt;

&lt;p&gt;None of these are hard on their own. Together they're a small, stateful, concurrency-sensitive system that sits in front of your most expensive operations, and it changes every time your pricing changes. That's the part that rarely makes it into the original estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;This is the problem we're building Meterbase for. You set up meters, plans and allowances once, and your code makes one call before the work and one after:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Meterbase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mbase-sdk&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;meterbase&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;Meterbase&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;METERBASE_API_KEY&lt;/span&gt;&lt;span class="o"&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;check&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;meterbase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acct_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// an estimate&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&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;`Refused: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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;reply&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;generateReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;meterbase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acct_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&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;total_tokens&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;Allowances, top-ups, plan changes, holds, rate limits and threshold webhooks sit behind those two calls. Meterbase isn't a billing system: Stripe or Paddle still take the money, and you read each customer's usage for a period to bill from.&lt;/p&gt;

&lt;p&gt;If you're building this right now, or you already built it and are tired of maintaining it, we'd love to hear how you handled it: &lt;a href="https://meterbase.tech" rel="noopener noreferrer"&gt;meterbase.tech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
