-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
blog: restyle docs.litellm.ai/blog to engineering blog aesthetic #25580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
05d5164
restyle blog list page to match engineering blog aesthetic
ishaan-berri 85cb7db
blog list page: Ramp-style flat list with hero, provider marquee, hir…
ishaan-berri dac44fb
blog list styles: clean typography, marquee animation, hero layout
ishaan-berri 8e616ec
add BlogPostPage swizzle: hide sidebar, add hiring CTA on every post
ishaan-berri 14eed24
add redis circuit breaker blog post with React diagrams
ishaan-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import React from 'react'; | ||
|
|
||
| const s = { | ||
| fig: {margin: '2.5rem 0', fontFamily: 'inherit'}, | ||
| box: {borderRadius: 12, border: '1px solid #e5e7eb', background: '#fff', padding: '2rem 2.5rem'}, | ||
| label: {fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: '#9ca3af', textAlign: 'center', marginBottom: '1.5rem'}, | ||
| caption: {textAlign: 'center', fontSize: 12, color: '#9ca3af', marginTop: 12}, | ||
| node: (border='#d1d5db', bg='#f9fafb') => ({ | ||
| border: `1px solid ${border}`, borderRadius: 6, padding: '8px 20px', | ||
| fontSize: 13, background: bg, display: 'inline-block', | ||
| }), | ||
| arrow: {display: 'flex', flexDirection: 'column', alignItems: 'center'}, | ||
| }; | ||
|
|
||
| const SmallArrow = ({color='#9ca3af'}) => ( | ||
| <svg width="2" height="28" style={{display:'block'}}> | ||
| <line x1="1" y1="0" x2="1" y2="22" stroke={color} strokeWidth="1.5"/> | ||
| <polygon points="1,28 -2,21 4,21" fill={color}/> | ||
| </svg> | ||
| ); | ||
|
|
||
| export function CascadeFailure() { | ||
| return ( | ||
| <figure style={s.fig}> | ||
| <div style={s.box}> | ||
| <p style={s.label}>Without circuit breaker — cascade failure</p> | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:0}}> | ||
| <div style={s.node()}>LiteLLM Pod (×100)</div> | ||
| <SmallArrow /> | ||
| <div style={s.node()}>Rate limit / cache check</div> | ||
| <div style={{position:'relative', display:'flex', flexDirection:'column', alignItems:'center'}}> | ||
| <SmallArrow color="#f87171"/> | ||
| <span style={{position:'absolute', left:8, top:4, fontSize:11, color:'#f87171', whiteSpace:'nowrap'}}>hangs 30s per request</span> | ||
| </div> | ||
| <div style={s.node('#fca5a5','#fef2f2')}><span style={{color:'#b91c1c', fontWeight:600}}>Redis — degraded, timing out</span></div> | ||
| <SmallArrow color="#fb923c"/> | ||
| <div style={s.node('#fdba74','#fff7ed')}><span style={{color:'#c2410c', fontWeight:600}}>Postgres — 100× normal read load</span></div> | ||
| <SmallArrow /> | ||
| <div style={{...s.node('#111827','#111827'), color:'#fff', fontWeight:600}}>Total outage — gateway down</div> | ||
| </div> | ||
| </div> | ||
| <figcaption style={s.caption}>Slow Redis → every auth check times out → database overwhelmed → full cascade</figcaption> | ||
| </figure> | ||
| ); | ||
| } | ||
|
|
||
| export function CircuitBreakerStates() { | ||
| const circle = (border, color, label, sub) => ( | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', width: 140}}> | ||
| <div style={{width:88, height:88, borderRadius:'50%', border:`2px solid ${border}`, background:'#fff', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center'}}> | ||
| <span style={{fontSize:11, fontWeight:700, color, letterSpacing:'0.06em'}}>{label}</span> | ||
| <span style={{fontSize:10, color:'#9ca3af', marginTop:2}}>{sub}</span> | ||
| </div> | ||
| <p style={{fontSize:11, color:'#6b7280', textAlign:'center', marginTop:10, lineHeight:1.5}}>{'\u00a0'}</p> | ||
| </div> | ||
| ); | ||
| const arrow = (label) => ( | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', marginTop:36, marginLeft:4, marginRight:4}}> | ||
| <span style={{fontSize:10, color:'#6b7280', marginBottom:4}}>{label}</span> | ||
| <div style={{display:'flex', alignItems:'center'}}> | ||
| <div style={{height:1, width:48, background:'#9ca3af'}}/> | ||
| <svg width="8" height="8" style={{marginLeft:-1}}><polygon points="0,0 8,4 0,8" fill="#6b7280"/></svg> | ||
| </div> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <figure style={s.fig}> | ||
| <div style={s.box}> | ||
| <p style={s.label}>Circuit breaker state machine</p> | ||
| <div style={{display:'flex', justifyContent:'center', alignItems:'flex-start'}}> | ||
| {circle('#1f2937','#111827','CLOSED','normal')} | ||
| {arrow('5 failures')} | ||
| {circle('#f87171','#dc2626','OPEN','fast-fail')} | ||
| {arrow('60s timeout')} | ||
| {circle('#fbbf24','#b45309','HALF-OPEN','probing')} | ||
| </div> | ||
| <div style={{display:'flex', justifyContent:'center', gap:32, marginTop:24}}> | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:4}}> | ||
| <div style={{display:'flex', alignItems:'center', gap:4}}> | ||
| <svg width="8" height="8"><polygon points="8,0 0,4 8,8" fill="#16a34a"/></svg> | ||
| <div style={{height:1, width:100, background:'#16a34a'}}/> | ||
| </div> | ||
| <span style={{fontSize:10, color:'#16a34a'}}>probe success → CLOSED</span> | ||
| </div> | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:4}}> | ||
| <div style={{display:'flex', alignItems:'center', gap:4}}> | ||
| <svg width="8" height="8"><polygon points="8,0 0,4 8,8" fill="#ef4444"/></svg> | ||
| <div style={{height:1, width:100, borderTop:'2px dashed #f87171'}}/> | ||
| </div> | ||
| <span style={{fontSize:10, color:'#ef4444'}}>probe failure → OPEN again</span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </figure> | ||
| ); | ||
| } | ||
|
|
||
| export function CircuitBreakerFlow() { | ||
| return ( | ||
| <figure style={s.fig}> | ||
| <div style={s.box}> | ||
| <p style={s.label}>With circuit breaker — graceful degradation</p> | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center'}}> | ||
| <div style={s.node()}>Incoming request</div> | ||
| <SmallArrow /> | ||
| <div style={{...s.node('#111827'), border:'2px solid #111827', fontWeight:600}}>Circuit Breaker</div> | ||
| <div style={{display:'flex', gap:80, marginTop:20, alignItems:'flex-start'}}> | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:8}}> | ||
| <SmallArrow /> | ||
| <span style={{fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em', color:'#6b7280', border:'1px solid #e5e7eb', borderRadius:4, padding:'2px 8px'}}>Closed</span> | ||
| <div style={{...s.node(), textAlign:'center', fontSize:13}}>Redis call<br/><span style={{fontSize:11, color:'#9ca3af'}}>normal latency</span></div> | ||
| </div> | ||
| <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:8}}> | ||
| <SmallArrow /> | ||
| <span style={{fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em', color:'#ef4444', border:'1px solid #fca5a5', borderRadius:4, padding:'2px 8px'}}>Open</span> | ||
| <div style={{...s.node('#fca5a5'), textAlign:'center', fontSize:13}}>Fast-fail — 0ms<br/><span style={{fontSize:11, color:'#9ca3af'}}>no network call</span></div> | ||
| <SmallArrow /> | ||
| <div style={{...s.node(), textAlign:'center', fontSize:13}}>DB fallback<br/><span style={{fontSize:11, color:'#9ca3af'}}>bounded load</span></div> | ||
| </div> | ||
| </div> | ||
| <div style={{...s.node('#111827','#111827'), color:'#fff', fontWeight:600, marginTop:24}}>Request completes — gateway stays up</div> | ||
| </div> | ||
| </div> | ||
| <figcaption style={s.caption}>Redis down → circuit opens → 0ms rejection → DB absorbs bounded fallback traffic</figcaption> | ||
| </figure> | ||
| ); | ||
| } | ||
|
|
||
| export function IncidentTimeline() { | ||
| const row = (color, text) => ( | ||
| <div style={{display:'flex', alignItems:'flex-start', gap:10, marginBottom:12}}> | ||
| <div style={{marginTop:5, width:6, height:6, borderRadius:'50%', background:color, flexShrink:0}}/> | ||
| <p style={{fontSize:13, color:'#4b5563', margin:0, lineHeight:1.5}}>{text}</p> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <figure style={s.fig}> | ||
| <div style={s.box}> | ||
| <p style={s.label}>Redis degrades — before vs. after</p> | ||
| <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:20}}> | ||
| <div style={{border:'1px solid #e5e7eb', borderRadius:8, padding:20}}> | ||
| <p style={{fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.1em', color:'#9ca3af', marginBottom:16}}>Without circuit breaker</p> | ||
| {row('#f87171','All 100 pods hang for 30s on each auth check')} | ||
| {row('#f87171','Threadpools fill up, requests queue')} | ||
| {row('#f87171','100× simultaneous DB fallbacks overwhelm Postgres')} | ||
| {row('#f87171','Requires manual intervention to recover')} | ||
| </div> | ||
| <div style={{border:'1px solid #111827', borderRadius:8, padding:20}}> | ||
| <p style={{fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.1em', color:'#9ca3af', marginBottom:16}}>With circuit breaker</p> | ||
| {row('#111827','Circuit opens after 5 failures — 0ms fast-fail')} | ||
| {row('#111827','Auth falls back to DB — bounded, not 100× load')} | ||
| {row('#111827','Cache miss rate temporarily elevated — gateway stays up')} | ||
| {row('#111827','Auto-recovers when Redis comes back — no intervention needed')} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </figure> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| --- | ||
| slug: redis-circuit-breaker | ||
| title: "Making the AI Gateway Resilient to Redis Failures" | ||
| date: 2026-04-11T09:00:00 | ||
| authors: | ||
| - ishaan | ||
| description: "We built a circuit breaker into LiteLLM so a slow Redis never cascades into a gateway outage. Here's how it works." | ||
| tags: [reliability, redis, infrastructure, engineering] | ||
| hide_table_of_contents: true | ||
| --- | ||
|
|
||
| import { CascadeFailure, CircuitBreakerStates, CircuitBreakerFlow, IncidentTimeline } from './diagrams'; | ||
|
|
||
| Redis is in the hot path for almost every request through LiteLLM: rate limiting, cache lookups, spend tracking. When Redis is healthy, the latency contribution is single-digit milliseconds. When it degrades, you need a plan - not just for when Redis is fully down, but for when it's *slow*. | ||
|
|
||
| Running an AI gateway at scale across 100+ pods means designing for failure modes before they show up in production. The dangerous case is not Redis being fully down. A complete outage is easy to handle - fail fast, fall through to the database, continue. The dangerous case is a *slow* Redis: still up, still accepting connections, but timing out after 20-30 seconds on each operation. | ||
|
|
||
| {/* truncate */} | ||
|
|
||
| ## Why slow Redis is harder than down Redis | ||
|
|
||
| <CascadeFailure /> | ||
|
|
||
| With 100 pods each hanging for 30 seconds on every auth check, the threadpool fills up. Requests queue. By the time Redis times out and falls through to Postgres, the database is receiving 100x its normal load from simultaneous fallbacks. A slow Redis becomes a database outage becomes a full gateway outage. | ||
|
|
||
| ## The fix: circuit breaker | ||
|
|
||
| The circuit breaker pattern solves this by tracking consecutive failures and cutting off the unhealthy dependency before it can cascade. Instead of hanging for 30 seconds on each Redis call, the circuit opens after 5 consecutive failures and fast-fails immediately - 0ms, no network call. | ||
|
|
||
| <CircuitBreakerStates /> | ||
|
|
||
| Three states: | ||
|
|
||
| - **CLOSED** - normal. All Redis calls pass through. | ||
| - **OPEN** - Redis is unhealthy. Fast-fail every call instantly. The request continues with degraded-but-functional behavior (DB fallback for auth). | ||
| - **HALF-OPEN** - after 60 seconds, one probe request is allowed through to test recovery. Success closes the circuit; failure resets the timer. | ||
|
|
||
| ## How requests flow through it | ||
|
|
||
| <CircuitBreakerFlow /> | ||
|
|
||
| When the circuit is open, the gateway does not stall. Auth checks fall back to Postgres - slower, but bounded. The database can handle the load because it is receiving *some* requests via DB, not *all* requests via DB simultaneously after 100 pods each waited 30 seconds for Redis to fail. | ||
|
|
||
| The difference: controlled degradation vs. uncontrolled cascade. | ||
|
|
||
| ## The implementation | ||
|
|
||
| ```python | ||
| class RedisCircuitBreaker: | ||
| def __init__(self, failure_threshold: int, recovery_timeout: int): | ||
| self.failure_threshold = failure_threshold # default: 5 | ||
| self.recovery_timeout = recovery_timeout # default: 60s | ||
| self._failure_count = 0 | ||
| self._state = self.CLOSED | ||
|
|
||
| def is_open(self) -> bool: | ||
| if self._state == self.OPEN: | ||
| if time.time() - self._opened_at > self.recovery_timeout: | ||
| self._state = self.HALF_OPEN | ||
| return False # this caller is the recovery probe | ||
| return True # fast-fail | ||
| return False | ||
|
|
||
| def record_failure(self): | ||
| self._failure_count += 1 | ||
| self._opened_at = time.time() | ||
| if self._failure_count >= self.failure_threshold: | ||
| self._state = self.OPEN # open the circuit | ||
|
|
||
| def record_success(self): | ||
| self._failure_count = 0 | ||
| self._state = self.CLOSED # Redis recovered | ||
| ``` | ||
|
|
||
| Every async Redis operation goes through a decorator that checks the breaker before touching the network. When open, it raises immediately: | ||
|
|
||
| ```python | ||
| @_redis_circuit_breaker_guard | ||
| async def async_get_cache(self, key: str): | ||
| ... | ||
| ``` | ||
|
|
||
| The decorator handles the bookkeeping - success increments nothing, failure increments the counter, exceptions trigger `record_failure()`. The caller sees a clean exception and falls through to its normal non-Redis path. | ||
|
|
||
| ## What this looks like in production | ||
|
|
||
| <IncidentTimeline /> | ||
|
|
||
| Redis degradation events no longer cascade. The observable symptom during a Redis slowdown is a temporary bump in cache miss rate - the right failure mode. Auth still works, rate limiting still works (at slightly higher DB cost), and recovery is fully automatic when Redis comes back. | ||
|
|
||
| ```bash | ||
| # configure via environment variables | ||
| REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD=5 # failures before opening | ||
| REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT=60 # seconds before probe | ||
| ``` | ||
|
|
||
| The circuit breaker is on by default in all LiteLLM versions since `v1.82.0`. No configuration needed for most deployments. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.blog-wrapper article .markdown preis forced tobackground: #ffffff !important, but there is no corresponding[data-theme='dark']override forpre— only inlinecodegets one (lines 912–916). The!importantflag beats Docusaurus's own dark-theme stylesheet, so every code block in a blog post renders with a bright white box in dark mode.