People need to see their remaining allowance before they run out.
Updated September 12, 2026
Problem
People can run out of usage without warning if the product does not show their remaining allowance or when it resets.
Solution
Show the remaining allowance in a unit people understand, along with its reset time. Explain when some models or modes use substantially more of it.
Make usage easy to check, and draw attention to it before people reach the limit. Show relevant costs where people choose an expensive option.
At the limit, explain what happened and what remains available. Offer supported options, such as waiting for a reset or using a lower-cost mode. For shared allowances, show enough detail to explain team usage without exposing private work.
Use when
The product has usage limits people may reach, especially when requests consume different amounts.
Don’t use when
The limit is high enough that ordinary use never approaches it. Displaying a meter that reads full forever trains people to ignore the one indicator that would matter, and makes a generous product feel stingy.
Trade-offs
A prominent meter can discourage useful exploration. Hiding it makes limits feel sudden. Technical units may be precise but hard to translate into work, while estimates need to be labeled clearly.
Checklist
Is the remaining budget visible before it becomes urgent?
Is the reset time stated precisely?
Are expensive actions marked at the point of choice?
Is the unit something a viewer can turn into an expectation of work?
Does a stopped or regenerated response still count, and is that said?
On a shared plan, can the viewer see whose usage applied?
Is exhaustion a designed state with a path forward?
Is there a cheaper option that still works at the limit?
Does reasoning or tool use count separately from the visible answer?
Does the meter stay quiet during the sessions that never approach the ceiling?
Compare
Claude publishes its rate limits and reset behaviour in documentation, so the ceiling is a checkable property rather than something discovered in use. ChatGPT meters by model tier and degrades to a lesser model at the limit. The session stays alive, and the drop in quality is the signal that a limit was hit. Perplexity counts the expensive mode separately from ordinary questions, which makes the costly action legible as its own budget rather than a silent drain on a shared one. GitHub Copilot meters premium requests against a monthly allowance and shows the consumption per request type. Nothing else here is itemised so finely.
Related
Model picker and Mode switch are where people choose options with different costs. Regenerate uses more allowance for another attempt.
Refusal describes other reasons a request cannot be completed. Context meter tracks the separate limit on how much information the model can use at once.
Wireframe — the pattern's anatomy, not any one product's version of it
Implementation
A quota bar that appears at three-quarters rather than at the wall, with the reset time, the per-action cost at the point of choice, and a colleague's share on a team plan. Reasoning and tool calls count, and are invisible.
a stopped response still consumed what it wrote: 6 credits
UsageMeter.tsxRenders nothing under three-quarters. Reset, per-action cost and the fallback are required props; a stopped response is charged and says so.
import { Button } from "@/components/ui/button";/** Who consumed what. On a team plan the colleague's share is the difference * between a product problem and a people problem. */export type Share = { who: string; used: number };/** A thing the viewer can choose, with what it costs. The cost is required: * marking it here, while they can still decide, is worth more than any * accuracy in the bar. */export type Action = { id: string; label: string; cost: number };/** Shown from three-quarters consumed. Always-on is ignored, and makes an * unbounded product feel metered. */const APPEARS_AT = 0.75;const SHARE_COLOR = ["bg-scale-seq-3", "bg-scale-seq-2", "bg-scale-seq-1"];const resetLabel = (at: Date, now: Date) => { const days = (at.getTime() - now.getTime()) / 86_400_000; return days < 7 ? at.toLocaleString("en-GB", { weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "UTC" }) : at.toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" });};export function UsageMeter({ limit, unit = "credit", shares, resetsAt, now = new Date(), actions, onChoose, fallback, stoppedCost }: { limit: number; /** Something the viewer can turn into work. "credit", not "token". */ unit?: string; /** The viewer first. */ shares: Share[]; /** A limit with no reset is a wall. */ resetsAt: Date; /** The clock to count against. Pass one to render on a server. */ now?: Date; actions: Action[]; onChoose: (id: string) => void; /** What still works once the budget is gone. Exhaustion is a designed state. */ fallback: string; /** What the last stopped response cost, if one was stopped. It counted. */ stoppedCost?: number;}) { const used = shares.reduce((n, s) => n + s.used, 0); if (used / limit < APPEARS_AT) return null; const left = Math.max(0, limit - used); const plural = (n: number) => `${n} ${unit}${n === 1 ? "" : "s"}`; return ( <div className="text-xs text-muted-foreground"> <div className="flex h-4 w-full overflow-hidden rounded-sm bg-muted" role="progressbar" aria-valuenow={used} aria-valuemax={limit} aria-label={`${used} of ${limit} ${unit}s used`}> {shares.map((s, i) => ( <span key={s.who} className={SHARE_COLOR[i % SHARE_COLOR.length]} style={{ width: `${(s.used / limit) * 100}%` }} /> ))} </div> <div className="mt-2 flex flex-wrap gap-x-4 tabular-nums"> {shares.map((s) => <span key={s.who}>{s.who}, {s.used}</span>)} <span className="ml-auto">resets {resetLabel(resetsAt, now)}</span> </div> <div className="mt-4 flex flex-wrap gap-3 border-t pt-4"> {actions.map((a) => { const dear = a.cost > left; return ( <Button key={a.id} variant="outline" onClick={() => onChoose(a.id)} disabled={dear} className={`h-auto flex-col items-start gap-1 px-3 py-2 ${a.cost > 1 ? "border-chart-1 bg-chart-1/10 text-chart-1 hover:bg-chart-1/20 hover:text-chart-1" : "bg-muted"}`}> <span>{a.label}</span> <span className="text-[11px] font-normal tabular-nums">{plural(a.cost)}{dear && ", more than is left"}</span> </Button> ); })} <p className="flex-1 basis-40 self-stretch rounded-md border bg-muted px-3 py-2 text-card-foreground"> at the limit, {fallback} still answers </p> </div> {stoppedCost !== undefined && ( <p className="mt-4">a stopped response still consumed what it wrote: {plural(stoppedCost)}</p> )} </div> );}
demo.tsxHow it is called: a 600-credit team plan with 452 spent. Choosing an action spends its cost.
import { useState } from "react";import { UsageMeter, type Share } from "./UsageMeter";/** * A team plan of 600 credits with 452 gone, so the meter has appeared. The * viewer used 300, a colleague 152, and a response stopped a moment ago still * cost 6. Choosing an action spends its cost against the viewer's share. */const NOW = new Date("2026-09-15T10:00:00Z");export default function Demo() { const [shares, setShares] = useState<Share[]>([ { who: "you", used: 300 }, { who: "a colleague", used: 152 }, ]); const spend = (cost: number) => setShares(([you, ...rest]) => [{ ...you, used: you.used + cost }, ...rest]); return ( <div className="rounded-lg border bg-card p-4"> <UsageMeter limit={600} shares={shares} resetsAt={new Date("2026-09-22T09:00:00Z")} now={NOW} actions={[ { id: "answer", label: "answer now", cost: 1 }, { id: "research", label: "research", cost: 40 }, ]} onChoose={(id) => spend(id === "research" ? 40 : 1)} fallback="the fast model" stoppedCost={6} /> </div> );}
What it renders. Identical markup in both panes, with only the token values changing.