Claude API Tokens, Pricing and Rate Limits

Claude API & Tool Use with Angular and Node.js · Getting started with Claude · 12 min read · Updated

Every request to the Claude API costs a little money, and the cost depends on tokens. If you understand tokens, you can predict your bill, avoid surprises and design features that are cheap to run. This lesson explains tokens in simple words and then adds a Tokens page to your AI Lab that counts tokens and shows the cost of each request.

What is a token?

Claude does not read text letter by letter or word by word. It first cuts your text into small pieces called tokens. A token can be a short word, a piece of a long word, a number or a punctuation mark. As a rough guide for English, one token is a bit less than one word, so 100 words are around 130 tokens. Other languages and special text, like code or unusual names, often use more tokens for the same length.

You do not need to count by hand. The API tells you exactly how many tokens each request used, and it can even count the tokens of a text before you send it.

Input tokens and output tokens

Every request has two sides, and they are priced separately.

  • Input tokens are everything you send: the system prompt, the whole chat history, any documents, and the definitions of tools. Even if the user types one short line, the history behind it may be long.
  • Output tokens are what Claude writes back. They usually cost more per token than input tokens, because writing is more work than reading.

Think of a tailor. Reading your measurements (input) is quick. Cutting and sewing the cloth (output) takes more effort, so it costs more. This is why a very long answer is expensive, and why we use max_tokens to set a ceiling.

Where does the price come from?

Anthropic publishes a price for each model, usually per million tokens, with one price for input and one for output. Prices change, and different models have different prices, so you should always copy the current numbers from the official pricing page. In the AI Lab we do not write any price into the code. You put your own numbers into the .env file (PRICE_INPUT_PER_MTOK and PRICE_OUTPUT_PER_MTOK). Then the cost formula is easy:

cost = (input tokens ÷ 1,000,000 × input price) + (output tokens ÷ 1,000,000 × output price)

For example, if a request used 2,000 input tokens and 500 output tokens, you work out the input part and the output part separately, and add them. A single request is usually a tiny amount. The danger is volume: a thousand users making ten requests each every day adds up quickly. That is why we track usage from day one.

Count tokens before you send

Sometimes you want to know the size of a request before paying for it. For example, you might check whether a long resume fits, or warn a user that a document is too big. The API has a token counting feature for this. It does not create an answer, so there is no output cost.

const result = await client.messages.countTokens({
  model: MODEL,
  messages: [{ role: 'user', content: longText }],
});
console.log(result.input_tokens);

Track usage and set budgets

Every answer includes a usage object. Good products record it. A simple habit is to log, for every request: which feature made it, which user made it, the input tokens, the output tokens, and the time. With this log you can answer important questions later. Which feature costs the most? Which user is making unusual numbers of requests? Did the new prompt we deployed yesterday double our cost?

  • Show usage in an admin dashboard so you see it every day.
  • Give each user or plan a quota, for example a number of AI actions per day.
  • Set an alert when the daily spending jumps.
  • Where the provider allows it, set a monthly spending cap in the console as a safety net.

Rate limits

The API also limits how fast you can use it. These rate limits are measured in requests per minute and tokens per minute. If you go over, the API answers with an error, usually status 429, which means "too many requests". You should not panic or retry instantly in a loop. The right reaction is to wait a short time and try again, and to design your product so that a sudden crowd does not hit the limit all at once. Lesson 6 covers retries properly. For now, know that limits exist, that they depend on your account, and that they can be raised as you grow.

Ways to spend less

  • Use the smallest model that passes your tests (lesson 1).
  • Keep prompts short. Remove old chat messages you no longer need.
  • Set a sensible max_tokens for each feature.
  • Use prompt caching for long text that repeats (lesson 7).
  • Use batch processing for work that can wait, such as scoring thousands of stored resumes overnight.
  • Do not call the API for something plain code can do. Counting words, checking an email format or removing spaces needs no AI.

Your task: build the Tokens page

Create a page that counts the tokens of any text, sends it to Claude, and keeps a usage log with the cost of each call and a running total.

What you will build

  • A POST /api/count route that returns the input token count using client.messages.countTokens.
  • A GET /api/pricing route that returns the prices from .env (or nulls if you have not set them).
  • A Tokens page with two buttons, "Count input tokens" and "Send to Claude", and a usage table with a total.

Steps

  1. Start from your lesson 2 project, or download the lesson 2 solution.
  2. Add PRICE_INPUT_PER_MTOK and PRICE_OUTPUT_PER_MTOK to .env. Copy the current numbers from Anthropic's pricing page for your model. Restart the server.
  3. Create server/routes/count.js with the two routes. Limit the text to 20,000 characters.
  4. Create TokensPage. On "Count", call /api/count and show the number. On "Send", call /api/ask (from lesson 2), and add the returned usage to a signal that holds a list.
  5. Write a cost() method that uses the prices from /api/pricing. If the prices are missing, show a hint instead of a number.
  6. Show a table with one row per call and a total. Try a short text and a long text and compare.

Hints

  • Use signal<Usage[]>([]) and calls.update(list => [...list, usage]) to add rows.
  • Divide by 1,000,000 before multiplying by the price per million.
  • Show the cost with six decimals, because single requests are tiny.

You know it works when the token count appears before sending, each real call adds a row with input and output tokens, and the total grows as you send more. As a bonus, try adding a "daily budget" field that turns the total red when you pass it.

Solution

The server routes:

import { Router } from 'express';
import { client, MODEL } from '../claude.js';

const router = Router();

// POST /api/count  { text }  ->  { inputTokens }   (does not generate anything, so no output cost)
router.post('/count', async (req, res) => {
  const text = String(req.body?.text || '').slice(0, 20000);
  if (!text.trim()) return res.status(400).json({ error: { message: 'Please type some text.' } });

  const result = await client.messages.countTokens({
    model: MODEL,
    messages: [{ role: 'user', content: text }],
  });
  res.json({ inputTokens: result.input_tokens });
});

// GET /api/pricing  ->  the prices YOU put in .env (we never hard-code prices)
const num = (v) => (v !== undefined && v !== '' && Number.isFinite(Number(v)) ? Number(v) : null);

router.get('/pricing', (req, res) => {
  res.json({
    inputPerMillion: num(process.env.PRICE_INPUT_PER_MTOK),
    outputPerMillion: num(process.env.PRICE_OUTPUT_PER_MTOK),
    currency: process.env.PRICE_CURRENCY || 'USD',
  });
});

export default router;

The Angular page:

import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { errorText } from '../util';

type Pricing = { inputPerMillion: number | null; outputPerMillion: number | null; currency: string };
type Usage = { input_tokens: number; output_tokens: number };

@Component({
  selector: 'app-tokens',
  imports: [FormsModule],
  template: `
    <h1>Tokens and cost</h1>
    <p class="hint">Count tokens before you send, then see what a real request used and what it cost.</p>

    <div class="card">
      <label for="t">Text</label>
      <textarea id="t" [(ngModel)]="text" placeholder="Paste a job description or a resume here..."></textarea>
      <button class="secondary" (click)="count()" [disabled]="busy() || !text.trim()">Count input tokens</button>
      <button (click)="send()" [disabled]="busy() || !text.trim()">Send to Claude</button>
      @if (error()) { <div class="err">{{ error() }}</div> }
      @if (counted() !== null) { <p>This text is about <strong>{{ counted() }}</strong> input tokens.</p> }
    </div>

    @if (calls().length) {
      <div class="card">
        <h2>Usage log</h2>
        <table>
          <thead><tr><th>#</th><th>Input</th><th>Output</th><th>Cost</th></tr></thead>
          <tbody>
            @for (c of calls(); track $index) {
              <tr><td>{{ $index + 1 }}</td><td>{{ c.input_tokens }}</td><td>{{ c.output_tokens }}</td><td>{{ cost(c) }}</td></tr>
            }
          </tbody>
        </table>
        <p><strong>Total this session: {{ totalCost() }}</strong></p>
        @if (!hasPrices()) {
          <p class="muted">Add PRICE_INPUT_PER_MTOK and PRICE_OUTPUT_PER_MTOK to your .env file to see money amounts.</p>
        }
      </div>
    }
  `,
})
export class TokensPage {
  private http = inject(HttpClient);

  text = '';
  busy = signal(false);
  error = signal('');
  counted = signal<number | null>(null);
  calls = signal<Usage[]>([]);
  pricing = signal<Pricing>({ inputPerMillion: null, outputPerMillion: null, currency: 'USD' });

  constructor() {
    firstValueFrom(this.http.get<Pricing>('/api/pricing')).then((p) => this.pricing.set(p)).catch(() => {});
  }

  hasPrices() {
    const p = this.pricing();
    return p.inputPerMillion !== null && p.outputPerMillion !== null;
  }

  cost(u: Usage): string {
    const p = this.pricing();
    if (!this.hasPrices()) return '-';
    const money = (u.input_tokens / 1_000_000) * p.inputPerMillion! + (u.output_tokens / 1_000_000) * p.outputPerMillion!;
    return `${money.toFixed(6)} ${p.currency}`;
  }

  totalCost(): string {
    const p = this.pricing();
    if (!this.hasPrices()) return '-';
    const money = this.calls().reduce(
      (sum, u) => sum + (u.input_tokens / 1_000_000) * p.inputPerMillion! + (u.output_tokens / 1_000_000) * p.outputPerMillion!,
      0,
    );
    return `${money.toFixed(6)} ${p.currency}`;
  }

  async count() {
    await this.run(async () => {
      const r = await firstValueFrom(this.http.post<{ inputTokens: number }>('/api/count', { text: this.text }));
      this.counted.set(r.inputTokens);
    });
  }

  async send() {
    await this.run(async () => {
      const r = await firstValueFrom(this.http.post<{ usage: Usage }>('/api/ask', { question: this.text }));
      this.calls.update((list) => [...list, r.usage]);
    });
  }

  private async run(job: () => Promise<void>) {
    this.busy.set(true);
    this.error.set('');
    try { await job(); } catch (e) { this.error.set(errorText(e)); } finally { this.busy.set(false); }
  }
}

Download the lesson 3 solution (zip)

What you learned

  • Text is split into tokens. You pay for input tokens and output tokens, at different prices.
  • countTokens tells you the size of a request before you send it.
  • The usage object lets you log cost per request and track spending.
  • Rate limits exist; handle them with patience and good design, not with instant retries.

In lesson 4 we move from one question to a real conversation with memory.

Frequently asked questions

Why was my bill higher than I expected?

Common causes are long chat history sent on every call, large documents, very long answers and retries. Log the usage of every request so you can see which feature is responsible.

What does a 429 error mean?

You hit a rate limit. Wait a little and try again with increasing delays, and design your app so that traffic is spread out.

Do I pay for counting tokens?

Counting tokens does not generate an answer, so there is no output cost. Check the current documentation for any limits on the counting feature.