Introduction
Software startups increasingly want to embed large language model (LLM) capabilities into their SaaS products. The challenge is not just calling an API, but designing a system that scales, respects latency budgets, and stays cost-effective. In this article I walk through a production-ready architecture that we have used at developerz.ai for multiple clients, covering data flow, caching, and monitoring.
1. Choose the right model and endpoint
Start by selecting a model that matches the task complexity. For simple text classification a smaller model such as gpt-3.5-turbo suffices, while code generation benefits from gpt-4-turbo. Use the providerβs streaming endpoint to reduce perceived latency and to allow partial results to be processed as they arrive.
2. Decouple request handling with a job queue
Directly calling the LLM from a web request can block the thread and increase response time. Instead, push the request onto a durable queue (e.g., Sidekiq or RabbitMQ) and return a lightweight acknowledgment to the client. A background worker then pulls the job, calls the LLM, and stores the result in a fast cache such as Redis.
# app/jobs/llm_job.rb
class LlmJob < ApplicationJob
queue_as:default
def perform(user_id, prompt)
response = LlmClient.generate(prompt)
Redis.current.set("llm:#{user_id}", response, ex: 300)
NotificationService.notify(user_id, response)
end
end
3. Cache deterministic responses
Many SaaS features involve repeated queries with identical prompts (e.g., generating a summary of a static document). Cache the LLM output keyed by a hash of the prompt. This reduces API calls, cuts cost, and improves latency.
cache_key = Digest::SHA256.hexdigest(prompt)
cached = Redis.current.get("llm_cache:#{cache_key}")
if cached
return JSON.parse(cached)
else
result = LlmClient.generate(prompt)
Redis.current.set("llm_cache:#{cache_key}", result.to_json, ex: 86_400)
result
end
4. Implement rate limiting and back-off
LLM providers enforce request limits. Wrap the client call in a retry block that respects Retry-After headers and applies exponential back-off. This prevents cascading failures during traffic spikes.
begin
LlmClient.generate(prompt)
rescue LlmClient::RateLimitError => e
sleep(e.retry_after || 2)
retry
end
5. Monitor usage and cost
Instrument each request with Prometheus metrics: request count, latency, and token usage. Export these metrics to a dashboard and set alerts when cost per day exceeds a threshold. This visibility helps the product team make informed decisions about feature pricing.
# prometheus.yml
- job_name: 'llm_requests'
static_configs:
- targets: ['localhost:9394']
6. Security and data privacy
Never send raw user data to the LLM. Strip personally identifiable information (PII) before constructing the prompt. If the provider offers a private endpoint or on-premise model, consider it for highly regulated domains.
7. Real-world example
A recent project involved a SaaS platform that generated weekly market analysis reports. By moving the LLM call to a background job, caching the generated sections, and limiting calls to 500 per day, we kept the monthly cost under $200 while delivering reports in under 5 seconds for the end user.
Conclusion
Embedding LLMs in SaaS products is more than a simple API call. A robust architecture separates concerns, caches results, respects rate limits, and provides observability. Following these patterns lets you ship AI features quickly without sacrificing reliability or cost control.
If you are planning an AI-powered feature and need a production-ready implementation, feel free to reach out. developerz.ai
Top comments (0)