CONCEPT · 60 minutes
Learn how to scale AI services when your real constraint is the API rate limit, not traffic — using queues, caches, and back-off strategies instead of more servers.
When I deployed the clinical-AI platform on a Mac Mini, I expected traffic to be my scaling problem. It wasn't.
The first time a few concurrent users hit the system at the same time, latency spiked from roughly 2 seconds to over 6. The server itself barely moved — CPU was idle. The problem was upstream: every user request fired a separate call to the Claude API, and the API's rate limit was already saturated.
A queue is a polite waiting room — better than everyone rushing the door at once. That one insight completely changed how I thought about scaling a solo AI service.
The key shift: Solo AI engineers don't need more servers. They need smarter usage of the single API they're calling.
The three levers that matter:
This module walks through each lever with working TypeScript patterns you can drop into a Next.js API route or a background service.
Who this module is for: You're running an AI service on a single machine or a small VPS. You call the Claude API (or another LLM API). You've hit a 429 error in production, or you're worried you will.
Anthropic enforces rate limits on two dimensions simultaneously:
When either limit is exceeded, the API returns HTTP 429 Too Many Requests along with a Retry-After header that tells you how long to wait.
Hitting one limit can hide the other. If you add retries only for 429s and your retry loop itself fires too fast, you can exhaust your RPM budget before your TPM budget recovers. Always back off on both dimensions.
Imagine a support-automation service where every incoming ticket triggers a Claude API call. At low volume (one ticket per minute) everything looks fine. But when five tickets arrive in thirty seconds:
The service isn't overloaded — the Mac Mini is idle. But from the API's perspective, the concurrency contract was violated.
Reflective prompt: Think about your own AI service (current or planned). What is the worst-case burst of concurrent users you'd expect in a five-minute window? How does that compare to your tier's RPM limit?
The cheapest API call is the one you never make. For AI services, caching has unusually high value because:
A simple in-memory LRU (Least Recently Used) cache handles the most common case — exact or near-exact repeated prompts.
Design notes worth internalizing:
Map preserves insertion order, so keys().next().value is always the oldest key."what is RAG?" and "what is RAG?" share the same cache entry. Add further normalization (lowercase, punctuation stripping) if your prompts are user-typed.Start here before anything else. Caching is the highest-ROI optimization available. In my support-automation work, a significant fraction of API calls during a session were variations on questions already answered earlier in the same conversation. Caching eliminated those calls entirely.
Caching helps with repeated requests. But what about a burst of new requests — ones that all need a live API call?
Without concurrency control, all those calls fire simultaneously and the API rate-limits you. The fix is a semaphore: a simple counter that limits how many calls can be in-flight at the same time.
Think of a semaphore like a restaurant with a fixed number of tables. New customers wait at the door if every table is full — they don't all try to squeeze in at once and knock the tables over.
The maxConcurrent value depends on your tier's RPM limit and your target response time. A conservative default for a solo production service is 3–5 concurrent calls. If your average call takes 2 seconds and you allow 3 concurrent, you're capped at roughly 90 RPM — comfortably under most tier limits.
Queue depth vs. timeout. The semaphore above queues requests indefinitely. In a user-facing flow, add a deadline: if a request waits more than a few seconds, reject it with a graceful message rather than making the user wait indefinitely.
Even with a semaphore, you may occasionally hit a 429 — perhaps your service and another app share the same API key, or there's a burst at startup. The correct response is exponential back-off with jitter.
Wrap the actual API call inside withBackoff to get automatic retry behaviour:
Sometimes the right answer is to fail quickly rather than retry. If a user is waiting for an interactive response and the API is saturated, queuing them for 30 seconds is worse than returning a friendly message immediately.
When to fail fast:
When to queue and wait:
Don't silently swallow errors. When you return a fallback, log the 429 event with a timestamp and the request context. A sudden spike in fallbacks is a signal that you need to either raise your API tier or reduce the maxConcurrent setting in your semaphore.
Here is how cache, semaphore, and back-off fit together in a production API route:
flowchart TD
A[Incoming Request] --> B{Cache hit?}
B -- Yes --> C[Return cached response]
B -- No --> D{Semaphore slot open?}
D -- No --> E[Wait in queue]
E --> F[Slot acquired]
D -- Yes --> F
F --> G[Call Claude API]
G -- Success --> H[Store in cache]
H --> I[Return response]
G -- 429 --> J{Retries left?}
J -- Yes --> K[Exponential back-off]
K --> G
J -- No --> L[Return fallback or error]
Scaling a solo AI service isn't about adding hardware. It's about respecting the constraints of the external API you depend on:
maxConcurrent value prevents burst traffic from firing too many parallel calls.These patterns cost nothing to implement and are often the difference between a stable service and one that is unexpectedly unavailable on a busy evening.
Test your understanding of API rate-limit management, caching, and back-off strategies for solo AI services.
1. Your Next.js API route calls Claude once per user request. You suddenly notice latency tripling and a spike in 429 errors after a feature launch. What is the most likely root cause?
2. Which combination of optimizations delivers the highest ROI for a solo AI service under rate-limit pressure?
3. An LRU cache with a 1-hour TTL will always serve stale responses to users after a model update.
True or False question
Correct Answer: FALSE
False. Stale responses are only served if the same prompt is repeated within the TTL window. You can also invalidate the cache on model updates by adding a model-version prefix to the cache key or clearing the cache explicitly.
4. You set a semaphore to maxConcurrent=5 and your average Claude API call takes 2 seconds. Roughly how many requests per minute can your service handle at steady state?
5. Why does exponential back-off need jitter (randomized delay) in addition to exponential growth?
6. A user submits a form that triggers an LLM call. The semaphore is full and the user would wait 20+ seconds. What is the best response?