Integrate AI with Rails: AI bootcamp for Developers – Day 8 – Production Hardening of the AI Integration

We have enough practical experience with SSE right now. We don’t need to perfect the transport layer, lets move on to improve our production error handling architecture.

Step 12 – Production Hardening of the AI Integration

We’ll cover this as one compact step:

LLM request
 ├── timeout
 ├── rate limit
 ├── provider error
 ├── invalid response
 ├── logging
 └── token/cost tracking

12.1 Add a custom AI error

Create:

app/services/ai/error.rb
class Ai::Error < StandardError
end

class Ai::ProviderError < Ai::Error
end

class Ai::RateLimitError < Ai::Error
end

class Ai::TimeoutError < Ai::Error
end

This gives our application its own error vocabulary instead of exposing SDK/provider exceptions everywhere.

12.2 Wrap the provider call

In Ai::Client, wrap the API call.

Conceptually:

def chat(messages:)
  response = @client.chat.completions.create(
    model: MODEL,
    messages: messages
  )

  {
    content: response.choices.first.message.content,
    model: response.model,
    input_tokens: response.usage&.prompt_tokens,
    output_tokens: response.usage&.completion_tokens
  }
rescue Faraday::TooManyRequestsError => e
  raise Ai::RateLimitError, e.message
rescue Faraday::TimeoutError => e
  raise Ai::TimeoutError, e.message
rescue Faraday::Error => e
  raise Ai::ProviderError, e.message
end

The exact exception classes can depend on the SDK/version, so inspect the exception raised by your installed openai gem rather than blindly copying provider-specific classes.

The important architecture is:

OpenRouter/SDK error
        ↓
Ai::Client
        ↓
Ai::RateLimitError
Ai::TimeoutError
Ai::ProviderError
        ↓
Rails application

Your controllers don’t need to know OpenRouter’s exception hierarchy.

12.3 Add timeout thinking

Never allow an AI request to hang indefinitely.

A production system should have:

connection timeout
read/request timeout

and then either:

retry

or:

fail gracefully

depending on the failure.

A key interview answer:

Retry transient failures such as timeouts and 429s with bounded exponential backoff, but don’t blindly retry all errors.

12.4 Token tracking

We’re already storing:

input_tokens
output_tokens

in messages.

That gives us an important operational capability:

conversation.messages.sum(:input_tokens)

and:

conversation.messages.sum(:output_tokens)

Now we can answer:

How many tokens did this conversation consume?

Later we can add pricing:

input tokens  × input price
+
output tokens × output price
=
estimated cost

Don’t hard-code provider pricing into the model. Pricing changes.

12.5 Add request timing

For a production AI application, latency is valuable.

In Ai::Client:

started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)

response = ...

latency_ms =
  ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round

Then eventually store:

latency_ms

on the message or in a separate AI usage/event table.

This allows:

model
tokens
latency
errors

to be correlated.

12.6 Don’t log prompts blindly

Avoid:

Rails.logger.info(params)

for AI endpoints.

User prompts may contain:

  • PII
  • secrets
  • customer information
  • proprietary company data

Log metadata instead:

conversation_id
model
latency
token counts
error type

rather than dumping the entire conversation into logs.

12.7 Add application-level rate limiting

An expensive AI endpoint should never be unrestricted.

Conceptually:

User
 ↓
Rate limit
 ↓
AI endpoint
 ↓
LLM

For example:

10 requests/minute/user

The exact limit depends on your application.

This protects:

  • cost
  • provider quotas
  • abuse
  • system capacity

12.8 What about retries?

Use something like:

Timeout      → retry
429          → retry with backoff
5xx          → retry with backoff
400          → don't retry
401          → don't retry
invalid input → don't retry

The exact mapping depends on the provider.

A useful int. phrase:

“I distinguish transient failures from permanent failures and use bounded retries with exponential backoff.”


Unknown's avatar

Author: Abhilash

Hi, I’m Abhilash! A seasoned web developer with 15 years of experience specializing in Ruby and Ruby on Rails. Since 2010, I’ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, Vue and React. Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Let’s explore the ever-evolving world of web development together!

Leave a comment