본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 16. 12:05

Ollama 프로덕션화: Rate Limits, Cloud Fallback, 그리고 비용 가드레일

요약

본 글은 로컬 환경에서 LLM을 구동하는 Ollama를 프로덕션 서비스에 적용할 때 발생하는 세 가지 주요 문제점(동시성 처리, 지연 시간 급증, 비용 관리)과 그 해결책을 제시합니다. 핵심적으로, Ollama는 네이티브 Rate Limiting 기능이 없어 동시 요청 시 GPU 자원을 직렬로 사용하며 성능 저하를 초래하기 때문에, SDK 레이어에 토큰 버킷(Token-bucket) 기반의 미들웨어를 구현하여 자체적인 속도 제한을 적용해야 합니다. 또한, 로컬 서비스가 과부하되거나 실패할 경우 클라우드 환경으로 자동으로 전환하는 다중 공급자 폴백(Multi-provider Fallback) 패턴을 구축하는 것이 중요합니다.

핵심 포인트

  • Ollama는 네이티브 Rate Limiting 기능이 없어 동시 요청 시 GPU 자원을 직렬로 처리하며 성능 저하를 유발한다.
  • 요청이 Ollama 프로세스에 도달하기 전에 SDK 레이어에서 토큰 버킷 기반의 미들웨어를 구현하여 자체적인 속도 제한(Throttling)을 적용해야 한다.
  • 로컬 서비스가 과부하되거나 실패할 경우, 클라우드 환경으로 자동으로 전환하는 다중 공급자 폴백(Multi-provider Fallback) 패턴을 구축해야 시스템 복원력을 높일 수 있다.

Ollama를 로컬에서 실행하는 것은 쉽습니다. 하지만 자신의 장비를 과부하시키지 않으면서 동시 접속 사용자를 처리하는 프로덕션 서비스에서 실행하는 것은 전혀 다른 문제입니다. 저는 'Running Local LLMs with NeuroLink and Ollama: Complete Guide'에서 기본적인 Ollama + NeuroLink 설정을 작성했습니다. 이 글은 그 후속편으로, 서비스를 배포하고 실제 트래픽이 발생하기 시작할 때 어떤 일이 벌어지는지에 대해 다룹니다. 세 가지 문제가 가장 먼저 발생합니다: 동시성(Concurrency) 상황에서 요청 큐(Request queues)가 쌓이고, 더 무거운 모델에서는 지연 시간(Latency)이 급증하며, "무료"라는 말이 "문제를 일으키지 않는다"는 뜻이 아님이 밝혀지면서 예산 가드레일(Budget guardrails)이 없게 됩니다. 이 세 가지를 모두 해결하는 방법을 소개합니다.

문제점: Ollama에는 네이티브 Rate Limiting이 없음
OpenAI는 Rate Limit(속도 제한)에 도달하면 429 에러를 반환합니다. Ollama에는 Rate Limit가 없습니다. 대신 요청을 큐에 쌓고 보유하고 있는 GPU에서 직렬(Serially)로 처리합니다. 단일 머신에서 llama3.1:70b 모델에 대해 5개의 동시 요청을 보내면, 다섯 번째 요청은 앞선 네 개의 요청이 완료될 때까지 기다려야 합니다. 실제로 이 경우 p99 지연 시간(Latency)이 4초에서 20초로 늘어나며 사용자는 포기하게 됩니다. 요청이 Ollama 프로세스에 도달하기 전, SDK 레이어에서 자체적인 Rate Limiting을 적용해야 합니다.

패턴 1: 미들웨어를 통한 요청 스로틀링 (Request Throttling)
NeuroLink의 미들웨어 시스템은 모든 generate() 호출 시 파이프라인으로서 실행됩니다. 스로틀링(Throttling) 미들웨어는 요청이 제공자(Provider)에게 전달되기 전에 요청을 거부하거나 큐에 쌓을 수 있습니다:

import { NeuroLink } from "@juspay/neurolink";

// 간단한 토큰 버킷(Token-bucket) 속도 제한기
class TokenBucket {
  private tokens: number;
  private lastRefill = Date.now();

  constructor(
    private readonly capacity: number,
    private readonly refillRatePerSecond: number
  ) {
    this.tokens = capacity;
  }

  consume(): boolean {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(
      this.capacity,
      this.tokens + elapsed * this.refillRatePerSecond
    );
    this.lastRefill = now;

    if (this.tokens >= 1) {
      this.

tokens -= 1 ; return true ; } return false ; } } const bucket = new TokenBucket ( 10 , 2 ); // 10 burst, 2 req/sec sustained const throttleMiddleware = { name : " ollama-throttle " , priority : 120 , // Runs before everything else transformParams : async ( params : any ) => { if ( ! bucket . consume ()) { throw new Error ( " LOCAL_RATE_LIMIT: Ollama request queue full " ); } return params ; }, }; const ai = new NeuroLink ({ provider : " ollama " , model : " llama3.1 " , middleware : [ throttleMiddleware ], }); The middleware throws a LOCAL_RATE_LIMIT error before the request reaches Ollama. Your calling code catches this and routes elsewhere — which brings us to the next pattern. Pattern 2: Falling Back to Cloud When Local is Overloaded This is the multi-provider fallback pattern from Building Resilient AI: Multi-Provider Fallback Patterns in TypeScript applied specifically to the Ollama overload scenario. NeuroLink's fallbackChain handles provider-level failures automatically, but the throttle middleware above throws before the provider is even called. You need to catch that specific error and escalate. Here's the full pattern: import { NeuroLink } from " @juspay/neurolink " ; // Primary: local Ollama with throttle const localAI = new NeuroLink ({ provider : " ollama " , model : " llama3.1 " , middleware : [ throttleMiddleware ], }); // Fallback: cloud providers in priority order const cloudAI = new NeuroLink ({ providers : [ { name : " anthropic " , model : " claude-3-5-haiku-20241022 " , priority : 1 }, { name : " openai " , model : " gpt-4o-mini " , priority : 2 }, ], fallbackChain : [ " anthropic " , " openai " ], }); async function generate ( prompt : string ) { try { return await localAI . generate ({ input : { text : prompt } }); } catch ( err : any ) { if ( err . message ?. startsWith ( " LOCAL_RATE_LIMIT " )) { // Ollama queue full — route to cloud console . warn ( " Ollama saturated, routing to cloud " ); return await cloudAI .

generate({ input: { text: prompt } }); } throw err; // 예상치 못한 오류는 다시 던집니다(Re-throw) } } const result = await generate("Summarize this support ticket..."); console.log(Provider used: ${result.provider});

여기서 중요한 점은 다음과 같습니다: 클라우드 폴백 (Cloud Fallback)으로 Claude Sonnet이나 GPT-4o가 아닌, Haiku나 GPT-4o-mini를 사용해야 한다는 것입니다. 폴백 (Fallback) 시나리오는 "Ollama가 바쁨" 상황입니다. 즉, 품질을 업그레이드하는 것이 아니라 오버플로 (Overflow)를 처리하는 것입니다. 가격 계층 (Price tier)이 아닌 성능 계층 (Capability tier)을 맞추십시오.

패턴 3: 지연 시간 예산 (Latency Budgets) — 타임아웃 (Timeout) 시 전환

큐 (Queue) 포화 상태만이 Ollama가 어려움을 겪고 있다는 유일한 신호는 아닙니다. 열 스로틀링 (Thermal throttling) 상태의 70B 모델은 요청을 수락할 수는 있지만 응답하는 데 30초가 걸릴 수도 있습니다. 따라서 지연 시간 예산 (Latency budget)이 필요합니다. NeuroLink의 generate()timeout 옵션 (숫자 ms 또는 "8s"와 같은 문자열)과 abortSignal을 허용하며, FallbackConfig 체인은 타임아웃 오류를 포함한 오류 발생 시 트리거됩니다. 깔끔한 지연 시간 예산 패턴을 위해 이 두 가지를 결합하십시오:

import { NeuroLink } from "@juspay/neurolink";

const ai = new NeuroLink({
  providers: [
    {
      name: "ollama",
      model: "llama3.1",
      priority: 1,
    },
    {
      name: "anthropic",
      model: "claude-3-5-haiku-20241022",
      priority: 2,
      apiKey: process.env.ANTHROPIC_API_KEY,
    },
  ],
  fallbackConfig: {
    enabled: true,
    maxAttempts: 2, // ollama, 그 다음 anthropic
    circuitBreaker: true,
  },
});

const result = await ai.generate({
  input: { text: prompt },
  timeout: 8000, // 호출에 대한 8초 예산; 오류 발생 시 → 폴백 (Fallback) 체인이 제어권을 가져감
});

// 어떤 프로바이더 (Provider)가 실제로 이 요청을 처리했는지 기록
if (result.provider !== "ollama") {
  console.warn(`Latency budget exceeded, fell back to ${result.provider}`);
  metrics.increment("ollama.latency_fallback");
}

타임아웃 (Timeout)을 보수적으로 설정하십시오. 대화형 요청에 대한 8초 예산은 채팅용으로는 이미 너무 느립니다. 실시간 인터페이스를 구축 중이라면 34초를 고려하고, 무거운 모델들이 빈번하게 폴백 (Fallback)될 수 있음을 받아들여야 합니다. 배치 처리 (Batch processing)의 경우에는 1530초 정도를 할당할 수 있습니다.

timeout 옵션은 전체 generate() 호출에 적용됩니다. 제공자별로 엄격한 마감 시간(예: "Claude와 경쟁하기 전에 Ollama에 정확히 3초를 부여")을 설정하려면, SDK가 제공자별 timeout 필드를 직접 노출하지 않으므로 각 제공자의 호출을 자체적인 AbortController와 함께 Promise.race로 감싸야 합니다.

패턴 4: onFinish 훅을 이용한 비용 가드레일 (Cost Guardrails)
"Ollama는 무료이다"라는 말은 LLM 호출 자체에 대해서는 사실입니다. 하지만 다음의 경우에는 사실이 아닙니다:

  • 클라우드 폴백 (Cloud fallback) 호출 (모든 Anthropic/OpenAI 요청에는 비용이 발생합니다)
  • 클라우드 GPU 인스턴스에서 Ollama를 실행할 경우 발생하는 컴퓨팅 비용
  • 조용히 돈을 쓰고 있는 서비스를 디버깅하는 데 드는 엔지니어링 시간

onFinish 라이프사이클 훅은 사용량 데이터와 제공자 정보와 함께 모든 성공적인 생성 후에 실행됩니다. 이를 사용하여 비용이 어디로 지출되는지 추적하세요:

import { NeuroLink } from "@juspay/neurolink";

// 1K 토큰당 가격 (클라우드 폴백 제공자)
const CLOUD_PRICING: Record<string, { input: number; output: number }> = {
  "claude-3-5-haiku-20241022": { input: 0.0008, output: 0.004 },
  "gpt-4o-mini": { input: 0.00015, output: 0.0006 },
};

let sessionCost = 0;
const BUDGET_ALERT_USD = 5.0; // 세션 지출이 $5에 도달하면 알림

const ai = new NeuroLink({
  providers: [
    { name: "ollama", model: "llama3.1", priority: 1 },
    {
      name: "anthropic",
      model: "claude-3-5-haiku-20241022",
      priority: 2,
      apiKey: process.env.ANTHROPIC_API_KEY,
    },
  ],
  fallback: true,
  fallbackConfig: { timeoutMs: 8000, retryAttempts: 1 },
  middleware: [
    {
      name: "cost-guard",
      onFinish: (result, metadata) => {
        // Ollama 비용은 사실상 0이지만, 훅은 여전히 실행됩니다
        const pricing = CLOUD_PRICING[metadata.model] ?? { input: 0, output: 0 };
        const callCost =
          ((result.usage?.promptTokens ?? 0) / 1000) * pricing.input +
          ((result.usage?.completionTokens ?? 0) / 1000) * pricing.output;
        
        sessionCost += callCost;

        // 항상 제공자를 로그로 남기세요 — 폴백 빈도를 가시화하는 것이 유용합니다
        console.

log([cost-guard] provider= ${metadata.provider} + model= ${metadata.model} + tokens= ${result.usage?.totalTokens ?? 0} + cost=$ ${callCost.toFixed(6)} + session_total=$ ${sessionCost.toFixed(4)}); if (metadata.provider !== "ollama") { metrics.increment("ollama.fallback_call", { provider: metadata.provider }); } if (sessionCost > BUDGET_ALERT_USD) { notifyOps(Cloud fallback cost alert: $ ${sessionCost.toFixed(2)} this session); } }, }, ], }); Ollama가 요청을 처리하더라도, 이 로그 라인을 통해 폴백(fallback) 비율을 확인할 수 있습니다. 만약 요청의 30%가 클라우드 폴백(cloud fallback)으로 넘어간다면, 현재의 Ollama 인스턴스는 트래픽에 비해 규모가 작게 설정된 것입니다.

종합하기: 프로덕션 준비가 된 Ollama 서비스
실제적인 트래픽을 처리하는 서비스의 전체 패턴은 다음과 같습니다:

import { NeuroLink } from "@juspay/neurolink";

const CLOUD_PRICING = {
"claude-3-5-haiku-20241022": { input: 0.0008, output: 0.004 },
"gpt-4o-mini": { input: 0.00015, output: 0.0006 },
};

const bucket = new TokenBucket(10, 2);

export const ai = new NeuroLink({
providers: [
{ name: "ollama", model: "llama3.1", priority: 1 },
{ name: "anthropic", model: "claude-3-5-haiku-20241022", priority: 2, apiKey: process.env.ANTHROPIC_API_KEY },
],
fallback: true,
fallbackConfig: {
timeoutMs: 8000,
retryAttempts: 1,
},
middleware: [
{
name: "throttle",
priority: 120,
transformParams: async (params: any) => {
if (!bucket.consume()) {
throw new Error("LOCAL_RATE_LIMIT");
}
return params;
},
},
{
name: "cost-guard",
onFinish: (result, metadata) => {
const pricing = (CLOUD_PRICING as any)[metadata.model] ?? { input: 0, output: 0 };
const cost = ((result.usage?.promptTokens ?? 0) / 1000) * pricing.input + ((result.usage?.completionTokens ?? 0) / 1000) * pricing.output;
recordMetrics({
provider: metadata.provider,
model: metadata.

model, tokens: result.usage?.totalTokens ?? 0, cost, duration: metadata.duration, wasLocal: metadata.provider === "ollama", }); }, onError: (error, metadata) => { logger.error("generation_failed", { provider: metadata.provider, error: error.message, recoverable: metadata.recoverable, }); }, }, ], }); export async function generateWithFallback (prompt: string) { try { return await ai.generate({ input: { text: prompt } }); } catch (err: any) { if (err.message?.startsWith("LOCAL_RATE_LIMIT")) { // 명시적인 큐 가득 참(queue-full) 경로: Ollama를 완전히 건너뛰고 즉시 클라우드로 이동 return await new NeuroLink({ providers: [ { name: "anthropic", model: "claude-3-5-haiku-20241022", apiKey: process.env.ANTHROPIC_API_KEY, }, ], }).generate({ input: { text: prompt } }); } throw err; } }

프로덕션(Production)에서 주의 깊게 살펴봐야 할 사항

추적할 가치가 있는 몇 가지 지표:

ollama.fallback_rate: Ollama에서 완료되지 않는 요청의 비율입니다. 10%가 넘는다면 인스턴스 규모가 너무 작은 것입니다.

ollama.p95_latency: 70B 모델의 p95(95퍼센타일) 지연 시간이 타임아웃 임계값을 초과한다면, 더 작은 모델을 사용하거나 더 많은 하드웨어가 필요합니다.

cloud_fallback.cost_per_hour: 오버플로(overflow) 요청으로 인해 발생하는 실제 클라우드 지출 비용입니다. 이것이 귀하의 실제 Ollama 인프라 비용입니다.

token_bucket.rejection_rate: Ollama를 시도하기도 전에 로컬 속도 제한(rate limit)에 걸리는 빈도입니다. 여기서 급증이 발생한다면 이는 하드웨어 문제가 아니라 보통 트래픽 폭주를 의미합니다.

Ollama 가이드는 무엇을 실행할지를 다룹니다. 이 설정은 실행한 후에 무엇을 관찰할지를 다룹니다.

NeuroLink로 시작하기:
GitHub: https://github.com/juspay/neurolink
npm: npm install @juspay/neurolink
Docs: https://blog.neurolink.ink/docs
Blog: https://blog.neurolink.ink

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0