Getting a DeepSeek API key and making your first successful call is the easy part. Running it in an actual product, where real users are sending real requests all day, is where the interesting problems start. You hit rate limits you did not plan for, your bill does not match what you expected, and when something goes wrong you have no way to tell whether it was your code, the network, or DeepSeek’s servers having a slow moment. This article covers what actually changes once you move past the first API call: handling rate limits properly, tracking cost per request instead of guessing from a monthly invoice, and adding enough observability that a production incident does not turn into a guessing game.

Handling rate limits without losing requests

DeepSeek’s API returns a standard 429 status code when you exceed your rate limit, along with response headers that tell you your current limit and how many requests or tokens you have left in the current window. The mistake most people make early on is either ignoring the 429 and letting the request fail, or retrying immediately, which usually just produces another 429.

The fix is a retry with exponential backoff, respecting the Retry-After header when the API sends one instead of guessing your own delay:

import time
import random
import requests

def call_deepseek_with_retry(payload, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.post(
            "https://api.deepseek.com/chat/completions",
            headers=headers,
            json=payload,
            timeout=30,
        )

        if response.status_code == 200:
            return response.json()

        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            if retry_after:
                delay = float(retry_after)
            else:
                delay = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(delay)
            continue

        if response.status_code >= 500:
            time.sleep((2 ** attempt) + random.uniform(0, 1))
            continue

        response.raise_for_status()

    raise RuntimeError(f"Failed after {max_retries} retries")

Notice that server errors (500 and above) get the same backoff treatment as rate limits, since a transient failure on DeepSeek’s side behaves the same way from your code’s point of view: the request failed, and retrying immediately makes it worse, not better. A 400 or 401, on the other hand, is not something a retry will fix, so those raise immediately instead of burning through your retry budget.

Queueing instead of retrying, at higher volume

Retry with backoff works fine for occasional bursts. If your application sends a steady, high volume of requests, for example a backend processing a queue of documents, the better fix is not to hit the limit in the first place. A simple token bucket rate limiter in front of your DeepSeek calls keeps you under the ceiling instead of reacting to it after the fact:

import time
import threading

class TokenBucket:
    def __init__(self, rate_per_second, capacity):
        self.rate = rate_per_second
        self.capacity = capacity
        self.tokens = capacity
        self.last_check = time.monotonic()
        self.lock = threading.Lock()

    def consume(self):
        with self.lock:
            now = time.monotonic()
            elapsed = now - self.last_check
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_check = now

            if self.tokens < 1:
                wait_time = (1 - self.tokens) / self.rate
                time.sleep(wait_time)
                self.tokens = 0
            else:
                self.tokens -= 1

Set rate_per_second a bit under your actual account limit, not exactly at it. DeepSeek's limits apply per account and can be shared across everything you run against that key, so leave headroom for anything else calling the same key, including a second service you forgot was using it.

Tracking cost per request, not per month

The invoice at the end of the month tells you what you spent. It does not tell you which feature, which customer, or which prompt is actually driving that number. Every DeepSeek chat completion response includes a usage object with prompt_tokens, completion_tokens, and total_tokens. Record these on every call, tagged with whatever dimension actually matters to your product:

def log_usage(response_json, feature_name, user_id):
    usage = response_json.get("usage", {})
    record = {
        "timestamp": time.time(),
        "feature": feature_name,
        "user_id": user_id,
        "model": response_json.get("model"),
        "prompt_tokens": usage.get("prompt_tokens", 0),
        "completion_tokens": usage.get("completion_tokens", 0),
        "total_tokens": usage.get("total_tokens", 0),
    }
    write_to_your_store(record)

Once this is flowing somewhere queryable, whether that is a Postgres table, a log index, or a proper observability backend, you can answer questions that a monthly invoice cannot: which feature in your product is actually expensive, whether a specific customer's usage pattern is disproportionate to what they are paying you, and whether a recent prompt change quietly increased average token usage per request. That last one is common and easy to miss. A small prompt tweak that adds a few extra instructions can raise your prompt token count on every single call, and that adds up fast at volume.

Check DeepSeek's current pricing page for the exact per-token rate for the model you are using, since it varies by model and can change. Multiply your logged token counts by that rate to get an actual cost per feature or per customer, rather than an estimate.

Making failures visible instead of silent

A DeepSeek call can fail in more ways than a clean error response. It can time out. It can return a 200 with a response body that does not have the shape you expected. It can succeed but take eight seconds when it normally takes one. None of these show up if the only thing your code does on failure is log a generic exception and move on.

Wrap the call in a span if you already have OpenTelemetry in your stack, recording the fields that actually help you debug an incident later:

from opentelemetry import trace

tracer = trace.get_tracer("deepseek.client")

def call_deepseek(payload, headers):
    with tracer.start_as_current_span("deepseek.chat_completion") as span:
        span.set_attribute("gen_ai.system", "deepseek")
        span.set_attribute("gen_ai.request.model", payload.get("model"))

        start = time.monotonic()
        try:
            response = call_deepseek_with_retry(payload, headers)
        except Exception as exc:
            span.set_attribute("error", True)
            span.set_attribute("error.message", str(exc))
            raise
        finally:
            span.set_attribute("gen_ai.response.duration_seconds", time.monotonic() - start)

        usage = response.get("usage", {})
        span.set_attribute("gen_ai.usage.input_tokens", usage.get("prompt_tokens", 0))
        span.set_attribute("gen_ai.usage.output_tokens", usage.get("completion_tokens", 0))
        return response

With this in place, a slow period shows up as a duration spike on deepseek.chat_completion spans instead of a vague complaint from a user that "the AI feature feels slow today." A spike in errors shows up as a spike in spans with error set to true, which you can alert on directly instead of parsing application logs after the fact.

A production checklist

  • Retry on 429 and 5xx with backoff, respecting Retry-After when it is present, and fail fast on 4xx errors that a retry cannot fix
  • Rate limit your own outbound calls if you run at steady high volume, instead of relying only on reactive retries
  • Log usage.prompt_tokens and usage.completion_tokens on every call, tagged by feature or customer, so cost is queryable instead of a single number on an invoice
  • Set a request timeout. DeepSeek's API is generally fast, but a hung connection with no timeout will eventually pile up and exhaust your own connection pool
  • Trace the call if you already have OpenTelemetry running, so latency and error spikes show up in the same place as the rest of your system's telemetry, not as a separate, disconnected concern

Frequently Asked Questions

How do I handle DeepSeek API rate limits in production?

Retry on a 429 response using exponential backoff, and respect the Retry-After header when the API includes one instead of guessing your own delay. For steady high volume traffic, add a token bucket rate limiter in front of your calls so you stay under the limit rather than reacting to it after each failure.

How can I track DeepSeek API cost per feature or per customer?

Every chat completion response includes a usage object with prompt_tokens and completion_tokens. Log these on every call tagged with the feature name or customer id, then multiply the totals by DeepSeek's current per-token price to get an actual cost figure broken down by whatever dimension matters to your product.

Should I retry every failed DeepSeek API call?

No. Retry on 429 rate limit responses and 5xx server errors, since those are usually transient. A 400 or 401 response means something is wrong with the request itself, such as a bad parameter or an invalid key, and retrying will not fix it, so those should fail immediately instead of consuming your retry budget.

You can also read this article on Medium.

Related Articles:

DeepSeek API Key: How to Get, Test & Troubleshoot It

Instrumenting an AI Agent with OpenTelemetry GenAI Conventions


Discover more from Tech Insights & Blogs by Rahul Ranjan

Subscribe to get the latest posts sent to your email.

Leave a Reply

Trending

Discover more from Tech Insights & Blogs by Rahul Ranjan

Subscribe now to keep reading and get access to the full archive.

Continue reading