AI Tutorial
Home All Courses Blog Interview Prep ↗ 0%
1
Curious builder 0 XP earned · 300 to level 2
0 days Finish a lesson to begin
Badge collection
0 of 5 unlocked
small wins to finish your path
Next lesson →

Cost, latency, and limits

A demo that costs a few cents and takes ten seconds is fine. A product serving thousands of users is not. Production LLM engineering is constrained by three numbers — money, time, and rate limits — and all three are measured in tokens. This lesson teaches you to estimate and track cost, measure and reduce latency, stay inside rate limits, and log the data you need to make good decisions.

Last updated: 01 Nov, 2026

Definitions

Definition · Token pricing

Providers charge separately for input tokens (everything you send) and output tokens (everything generated), usually quoted in US dollars per one million tokens. Output tokens typically cost 3–5× more than input tokens.

Definition · Latency

The time a user waits. For LLMs it has two parts: time to first token (TTFT) — how long until output starts — and generation time — output tokens divided by the model's speed in tokens per second.

Definition · Rate limit

A cap your provider enforces on usage per time window, typically requests per minute (RPM) and tokens per minute (TPM), sometimes also per day. Exceeding it returns HTTP 429 Too Many Requests.

Calculating cost

The formula is simple: cost = input_tokens × input_price + output_tokens × output_price, with prices per million tokens. The prices below are illustrative — always use your provider's current pricing page.

Python
PRICES_PER_MILLION = {          # (input $, output $) - example numbers only
    "small-fast-model": (0.15, 0.60),
    "mid-model": (0.40, 1.60),
    "frontier-model": (2.50, 10.00),
}


def call_cost(model: str, input_tokens: int, output_tokens: int) -> float:
    price_in, price_out = PRICES_PER_MILLION[model]
    return (input_tokens * price_in + output_tokens * price_out) / 1_000_000


# a RAG answer: 3,000 tokens of context + question, 400-token answer
for model in PRICES_PER_MILLION:
    one = call_cost(model, 3_000, 400)
    monthly = one * 20_000 * 30            # 20k requests per day
    print(f"{model:<17} ${one:.5f} per call   ${monthly:>9,.2f} per month")
small-fast-model $0.00069 per call $ 414.00 per month mid-model $0.00184 per call $ 1,104.00 per month frontier-model $0.01150 per call $ 6,900.00 per month

The same feature costs roughly 17× more on the frontier model. Notice also that input dominates here: 3,000 input tokens outweigh 400 output tokens even at a 4× higher output price. In RAG systems, retrieving fewer, better chunks is often the biggest cost lever.

Pricing detailWhat it means
Cached inputRepeated prompt prefixes (same system prompt, same documents) can be billed at a large discount — often 50–90% off — when the provider's prompt cache hits.
Batch APISubmitting non-urgent jobs to a batch endpoint, completed within 24 hours, typically costs about 50% less.
Reasoning tokensReasoning models generate hidden “thinking” tokens that are billed as output tokens even though you do not see them.
Images, audio, toolsImages and audio are converted to tokens; tool definitions are part of the input on every call.

Measuring latency

Formula
total latency ≈ network + queue + TTFT (prefill of input) + output_tokens / tokens_per_second

Stream the response to measure both parts. The first chunk that carries content marks the TTFT:

Python
import time

from openai import OpenAI

client = OpenAI()

start = time.perf_counter()
first_token_at = None
pieces = []

stream = client.chat.completions.create(
    model="gpt-4.1-mini",
    messages=[{"role": "user", "content": "Explain RAG in 80 words."}],
    stream=True,
    stream_options={"include_usage": True},     # final chunk carries token usage
)
usage = None
for chunk in stream:
    if chunk.usage:
        usage = chunk.usage
    if chunk.choices and chunk.choices[0].delta.content:
        if first_token_at is None:
            first_token_at = time.perf_counter()
        pieces.append(chunk.choices[0].delta.content)
end = time.perf_counter()

ttft = first_token_at - start
gen_time = end - first_token_at
print(f"TTFT: {ttft * 1000:.0f} ms")
print(f"total: {(end - start) * 1000:.0f} ms")
print(f"speed: {usage.completion_tokens / gen_time:.0f} tokens/s")
TTFT: 410 ms total: 2,190 ms speed: 61 tokens/s

Your numbers will vary with model, region, load, and prompt size. Measure on your own traffic and watch the p95 (the slowest 5% of requests), not just the average — users notice the slow tail.

How to reduce latency

TechniqueEffect
Stream the responseUsers see text after the TTFT instead of after the full generation — the single biggest perceived-speed win.
Ask for shorter outputOutput tokens are generated sequentially; halving answer length roughly halves generation time.
Use a smaller, faster modelSmall models generate many more tokens per second and have lower TTFT.
Shrink the promptLess prefill work; retrieve fewer chunks, trim history.
Keep prompt prefixes stableEnables prompt caching, which lowers both cost and TTFT.
Run independent calls in parallelTotal time becomes the slowest call instead of the sum.

Rate limits

Rate limits are set per account or project and usually grow automatically as you spend more. When you hit one, the API returns 429 and often tells you when to retry. Responses also include headers showing your remaining budget:

Response headers
x-ratelimit-limit-requests: 5000
x-ratelimit-remaining-requests: 4987
x-ratelimit-limit-tokens: 2000000
x-ratelimit-remaining-tokens: 1964210
x-ratelimit-reset-tokens: 1.07s
StrategyHow
Retry with exponential backoffSDKs do this automatically (max_retries). Respect Retry-After.
Limit concurrencyCap simultaneous requests with a worker pool or semaphore.
Reduce tokens per requestTPM is often the binding limit, not RPM.
Queue background workPush non-urgent jobs through a queue or the Batch API.
Distinguish quota from speedinsufficient_quota means billing, not too many requests — retrying will not help.

Controlled parallelism

Here is a pattern for processing many prompts concurrently while capping the number in flight. fake_call simulates a 0.2-second API call so you can run it offline.

Python
import time
from concurrent.futures import ThreadPoolExecutor


def fake_call(prompt: str) -> str:
    time.sleep(0.2)                      # stand-in for network + generation time
    return prompt.upper()


prompts = [f"summarise doc {i}" for i in range(10)]

start = time.perf_counter()
sequential = [fake_call(p) for p in prompts]
print(f"sequential: {time.perf_counter() - start:.1f}s")

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as pool:     # at most 5 requests in flight
    parallel = list(pool.map(fake_call, prompts))
print(f"parallel (5 workers): {time.perf_counter() - start:.1f}s")
print(parallel[:2], parallel == sequential)
sequential: 2.0s parallel (5 workers): 0.4s ['SUMMARISE DOC 0', 'SUMMARISE DOC 1'] True

pool.map returns results in the original order. Choose max_workers so that workers × tokens per request stays below your TPM limit.

Log every call

You cannot optimise what you do not measure. Record a structured log line for every request, then analyse it with pandas or your observability tool.

Python
import json
import time
from dataclasses import asdict, dataclass

PRICES_PER_MILLION = {"small-fast-model": (0.15, 0.60)}


@dataclass
class LLMCallLog:
    feature: str
    model: str
    input_tokens: int
    output_tokens: int
    latency_ms: float
    finish_reason: str
    cost_usd: float


def log_call(feature, model, usage, latency_s, finish_reason) -> LLMCallLog:
    price_in, price_out = PRICES_PER_MILLION[model]
    cost = (usage["prompt_tokens"] * price_in + usage["completion_tokens"] * price_out) / 1e6
    entry = LLMCallLog(feature, model, usage["prompt_tokens"], usage["completion_tokens"],
                       round(latency_s * 1000, 1), finish_reason, round(cost, 6))
    print(json.dumps(asdict(entry)))
    return entry


t0 = time.perf_counter()
time.sleep(0.05)                                   # pretend API call
fake_usage = {"prompt_tokens": 1200, "completion_tokens": 180}
entry = log_call("support-bot", "small-fast-model", fake_usage, time.perf_counter() - t0, "stop")
print(entry.cost_usd)
{"feature": "support-bot", "model": "small-fast-model", "input_tokens": 1200, "output_tokens": 180, "latency_ms": 55.0, "finish_reason": "stop", "cost_usd": 0.000288} 0.000288
FieldWhy log it
Feature / endpoint nameFind which product feature drives cost.
Model and versionCompare models; detect silent upgrades.
Input / output / cached tokensCost and context-size trends.
Latency (TTFT and total)Catch slowdowns and p95 regressions.
Finish reason and errorsSpot truncation, filtering, and failure rates.
User or tenant id (hashed)Detect abuse and allocate spend.
Watch out

Be careful about logging full prompts and responses: they may contain personal data or secrets. Log token counts and metadata by default, and store content only with a clear retention policy and access controls.

Caching responses

Many applications see the same question repeatedly. Caching the answer for identical requests costs nothing and returns instantly:

Python
import hashlib
import json

CACHE: dict[str, str] = {}
calls = {"api": 0}


def fake_llm(messages, model, temperature):
    calls["api"] += 1
    return f"answer to: {messages[-1]['content']}"


def cached_chat(messages, model="small-fast-model", temperature=0.0) -> str:
    key_src = json.dumps({"m": messages, "model": model, "t": temperature}, sort_keys=True)
    key = hashlib.sha256(key_src.encode()).hexdigest()
    if key not in CACHE:
        CACHE[key] = fake_llm(messages, model, temperature)
    return CACHE[key]


q = [{"role": "user", "content": "What are your opening hours?"}]
for _ in range(3):
    print(cached_chat(q))
print("API calls made:", calls["api"])
answer to: What are your opening hours? answer to: What are your opening hours? answer to: What are your opening hours? API calls made: 1

Only cache deterministic, non-personal requests (low temperature, no user-specific data). In production use Redis or a database with an expiry time instead of a Python dict. Semantic caching goes further by matching questions that are similar in meaning using embeddings.

Cost-reduction checklist

  1. Route by difficulty: send easy requests to a small model and only hard ones to a frontier model.
  2. Trim input: fewer retrieved chunks, summarised history, concise system prompts.
  3. Cap output: set max_tokens and ask for concise answers.
  4. Keep prefixes stable to benefit from prompt caching.
  5. Cache repeated answers.
  6. Batch offline jobs through the Batch API.
  7. Set budgets and alerts per project in the provider dashboard, plus your own per-user limits.

Other limits to know

LimitWhat to do
Context windowCount tokens; trim or summarise history; use retrieval.
Max output tokensSplit long generation into sections, or continue when finish_reason == "length".
Request timeoutSet client timeouts; stream long answers; use background jobs for very long tasks.
Payload size (files, images)Resize images; upload large files through the provider's file API.
Content policiesHandle content_filter results and refusals gracefully in the UI.
Try it
  1. Use call_cost to compare a 6,000-token RAG prompt with a 1,500-token one at 50,000 requests per day.
  2. Wrap your real API helper with timing and the log_call function, run ten prompts, and load the logs into a pandas DataFrame to get mean and p95 latency.
  3. Add the response cache to your helper and measure how many calls it saves on a list of questions with duplicates.
Key takeaways
  • Cost = input tokens × input price + output tokens × output price; input often dominates in RAG.
  • Latency = TTFT + output tokens ÷ speed; stream responses and keep answers short.
  • Rate limits are RPM and TPM; back off on 429, cap concurrency, and distinguish quota from speed.
  • Log model, tokens, latency, finish reason, and cost for every call.
  • Cut spend with model routing, shorter prompts, prompt caching, response caching, and batching.
  • Next: apply these fundamentals to writing reliable prompts in the Prompt Engineering course.

Where to go next

You have completed LLM Fundamentals. Continue with Prompt Engineering to turn this understanding into reliable prompts, then build grounded applications in RAG Systems.