From 05d516482f966a0d6b9ba62eac3ae41b546520fa Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 11 Apr 2026 18:02:44 -0700 Subject: [PATCH 1/5] restyle blog list page to match engineering blog aesthetic --- docs/my-website/src/css/custom.css | 120 +++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/docs/my-website/src/css/custom.css b/docs/my-website/src/css/custom.css index d0702fcc57c..29be18a5ae9 100644 --- a/docs/my-website/src/css/custom.css +++ b/docs/my-website/src/css/custom.css @@ -794,3 +794,123 @@ video { max-width: calc(9 / 12 * 100%) !important; } } + +/* ========================================= + BLOG — Ramp-style aesthetic + ========================================= */ + +/* Hide blog sidebar on post pages */ +.blog-post-page aside.col { + display: none !important; +} + +/* Make blog post content full-width + constrained */ +.blog-post-page main.col--7 { + --ifm-col-width: 100% !important; + max-width: 820px !important; + margin: 0 auto !important; + flex: 0 0 100% !important; +} + +/* Clean post header */ +.blog-wrapper article header h1 { + font-size: 2rem; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.25; + color: #111827; + margin-bottom: 0.75rem; +} + +/* Author / date line */ +.blog-wrapper article header .avatar, +.blog-wrapper article header [class*='blogPostData'] { + margin-top: 0.75rem; +} + +/* Clean prose body */ +.blog-wrapper article .markdown { + font-size: 0.95rem; + line-height: 1.7; + color: #374151; +} + +.blog-wrapper article .markdown h2 { + font-size: 1.35rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-top: 2.5rem; + margin-bottom: 0.75rem; + color: #111827; +} + +.blog-wrapper article .markdown h3 { + font-size: 1.1rem; + font-weight: 600; + margin-top: 2rem; + margin-bottom: 0.5rem; + color: #111827; +} + +.blog-wrapper article .markdown p { + margin-bottom: 1.25rem; +} + +.blog-wrapper article .markdown a { + color: #0ea5e9; + text-decoration: none; +} + +.blog-wrapper article .markdown a:hover { + text-decoration: underline; +} + +.blog-wrapper article .markdown code { + font-size: 0.85em; + background: #f3f4f6; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 0.15em 0.4em; + color: #111827; +} + +.blog-wrapper article .markdown pre { + background: #ffffff !important; + border: 1px solid #e5e7eb !important; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.blog-wrapper article .markdown pre code { + background: transparent; + border: none; + padding: 0; + color: inherit; +} + +/* Hide tags section at bottom of blog posts */ +.blog-wrapper footer [class*='blogPostTags'], +.blog-wrapper footer [class*='tags'] { + display: none; +} + +/* Nav buttons (prev/next) at bottom - keep clean */ +.blog-wrapper .pagination-nav__label { + font-size: 0.85rem; +} + +[data-theme='dark'] .blog-wrapper article header h1, +[data-theme='dark'] .blog-wrapper article .markdown h2, +[data-theme='dark'] .blog-wrapper article .markdown h3 { + color: #f9fafb; +} + +[data-theme='dark'] .blog-wrapper article .markdown { + color: #d1d5db; +} + +[data-theme='dark'] .blog-wrapper article .markdown code { + background: #1f2937; + border-color: #374151; + color: #f9fafb; +} From 85cb7db8b9ebd832aee25be54a3ace478d36045b Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 11 Apr 2026 18:02:48 -0700 Subject: [PATCH 2/5] blog list page: Ramp-style flat list with hero, provider marquee, hiring CTA --- .../src/theme/BlogListPage/index.js | 192 ++++++++++-------- 1 file changed, 103 insertions(+), 89 deletions(-) diff --git a/docs/my-website/src/theme/BlogListPage/index.js b/docs/my-website/src/theme/BlogListPage/index.js index 277556a3528..9c1844c7f7e 100644 --- a/docs/my-website/src/theme/BlogListPage/index.js +++ b/docs/my-website/src/theme/BlogListPage/index.js @@ -3,78 +3,86 @@ import Layout from '@theme/Layout'; import Link from '@docusaurus/Link'; import styles from './styles.module.css'; -const TAG_COLORS = { - gemini: {bg: '#d2e3fc', text: '#174ea6', darkBg: '#1a3a5c', darkText: '#8ab4f8'}, - anthropic: {bg: '#fde0c4', text: '#b33d00', darkBg: '#4a2800', darkText: '#ffb74d'}, - claude: {bg: '#fde0c4', text: '#b33d00', darkBg: '#4a2800', darkText: '#ffb74d'}, - llms: {bg: '#c8e6c9', text: '#1b5e20', darkBg: '#1b3d1f', darkText: '#81c784'}, -}; +// ── Provider marquee ────────────────────────────────────────────────────── +const PROVIDERS = [ + { name: 'OpenAI', img: 'https://www.google.com/s2/favicons?domain=openai.com&sz=64' }, + { name: 'Anthropic', img: 'https://www.google.com/s2/favicons?domain=claude.ai&sz=64' }, + { name: 'Google Gemini', img: 'https://www.google.com/s2/favicons?domain=ai.google.dev&sz=64' }, + { name: 'AWS Bedrock', img: 'https://www.google.com/s2/favicons?domain=aws.amazon.com&sz=64' }, + { name: 'Azure OpenAI', img: 'https://www.google.com/s2/favicons?domain=azure.microsoft.com&sz=64' }, + { name: 'Mistral AI', img: 'https://www.google.com/s2/favicons?domain=mistral.ai&sz=64' }, + { name: 'Meta Llama', img: 'https://www.google.com/s2/favicons?domain=meta.com&sz=64' }, + { name: 'Groq', img: 'https://www.google.com/s2/favicons?domain=groq.com&sz=64' }, + { name: 'Hugging Face', img: 'https://www.google.com/s2/favicons?domain=huggingface.co&sz=64' }, + { name: 'Perplexity', img: 'https://www.google.com/s2/favicons?domain=perplexity.ai&sz=64' }, + { name: 'DeepSeek', img: 'https://www.google.com/s2/favicons?domain=deepseek.com&sz=64' }, + { name: 'Cohere', img: 'https://www.google.com/s2/favicons?domain=cohere.com&sz=64' }, + { name: 'Together AI', img: 'https://www.google.com/s2/favicons?domain=together.ai&sz=64' }, + { name: 'Vertex AI', img: 'https://www.google.com/s2/favicons?domain=cloud.google.com&sz=64' }, +]; -function hashHue(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - return Math.abs(hash) % 360; -} - -function getTagColor(label) { - const key = label.toLowerCase(); - for (const [k, v] of Object.entries(TAG_COLORS)) { - if (key === k) return v; - } - const hue = hashHue(key); - return { - bg: `hsl(${hue}, 40%, 90%)`, - text: `hsl(${hue}, 60%, 25%)`, - darkBg: `hsl(${hue}, 40%, 20%)`, - darkText: `hsl(${hue}, 50%, 75%)`, - }; -} - -function formatDate(dateStr) { - const d = new Date(dateStr); - const now = new Date(); - const diffDays = Math.floor((now - d) / (1000 * 60 * 60 * 24)); - if (diffDays <= 0) return 'Today'; - if (diffDays === 1) return '1d ago'; - if (diffDays < 30) return `${diffDays}d ago`; - return d.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric'}); -} - -function BlogCard({post, featured}) { - const {title, permalink, date, description, tags} = post; - const visibleTags = (tags || []).slice(0, 3); +const DOUBLED = [...PROVIDERS, ...PROVIDERS]; +function ProviderMarquee() { return ( - -
-
- - {featured && Latest} +
+

Routing to 100+ providers

+
+
+
+
+ {DOUBLED.map((p, i) => ( + + {p.name} + {p.name} + | + + ))}
+
+
+ ); +} + +// ── Post row ────────────────────────────────────────────────────────────── +function formatDate(dateStr) { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'long', day: 'numeric', year: 'numeric', + }); +} + +function AuthorList({authors}) { + if (!authors || authors.length === 0) return null; + return ( + <> + {authors.map((a, i) => ( + + {i > 0 && } + {a.url ? ( + {a.name} + ) : ( + {a.name} + )} + + ))} + + ); +} + +function PostRow({post}) { + const {title, permalink, date, description, authors} = post; + return ( +
+

{title}

- {description &&

{description}

} - {visibleTags.length > 0 && ( -
- {visibleTags.map(tag => { - const c = getTagColor(tag.label); - return ( - {tag.label} - ); - })} -
- )} - -
- + + {description &&

{description}

} +
+ + {authors && authors.length > 0 && } + +
+
); } @@ -83,41 +91,47 @@ function Pagination({metadata}) { if (!previousPage && !nextPage) return null; return ( ); } +// ── Page ────────────────────────────────────────────────────────────────── export default function BlogListPage(props) { const items = props.items || []; const metadata = props.metadata || {}; - const [first, ...rest] = items; return ( -
-

The LiteLLM Blog

-

Guides, announcements, and best practices from the LiteLLM team.

-
+
+ {/* Hero */} +
+

AI Gateway

+

Engineering

+

+ How we build the world's most widely used open-source AI Gateway. + Routing, reliability, observability, and what we learn along the way. +

+ + We're hiring! + +
-
- {first && ( - - )} - {rest.map(({content}) => ( - - ))} -
+ - + {/* Post list */} +
+ {items.map(({content}) => ( + + ))} +
+ + +
); } From dac44fb443e0f98efeb045bc4e4e450285041210 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 11 Apr 2026 18:02:52 -0700 Subject: [PATCH 3/5] blog list styles: clean typography, marquee animation, hero layout --- .../src/theme/BlogListPage/styles.module.css | 353 +++++++++++------- 1 file changed, 222 insertions(+), 131 deletions(-) diff --git a/docs/my-website/src/theme/BlogListPage/styles.module.css b/docs/my-website/src/theme/BlogListPage/styles.module.css index 747c9846a2c..520e4c41c61 100644 --- a/docs/my-website/src/theme/BlogListPage/styles.module.css +++ b/docs/my-website/src/theme/BlogListPage/styles.module.css @@ -1,163 +1,254 @@ -.hero { - max-width: 960px; +/* ── Page shell ───────────────────────────────────────────────────────── */ +.page { + max-width: 860px; margin: 0 auto; - padding: 3rem 1.5rem 1rem; - text-align: center; + padding: 0 2rem; +} + +/* ── Hero ─────────────────────────────────────────────────────────────── */ +.hero { + padding: 3.5rem 0 0; +} + +.eyebrow { + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: #0ea5e9; + margin: 0 0 0.5rem; } .heroTitle { - font-size: 2.25rem; - font-weight: 700; - margin-bottom: 0.25rem; - letter-spacing: -0.02em; -} - -.heroSubtitle { - color: var(--ifm-color-emphasis-600); - font-size: 1.1rem; - margin-bottom: 0; -} - -.grid { - max-width: 960px; - margin: 0 auto; - padding: 1.5rem; - display: grid; - gap: 1rem; -} - -.cardLink { - display: block; - text-decoration: none; - color: inherit; -} - -.card { - position: relative; - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 12px; - padding: 1.5rem; - padding-right: 2.5rem; - height: 100%; - transition: border-color 0.15s, transform 0.15s, background 0.15s; - background: var(--ifm-background-surface-color, var(--ifm-background-color)); -} - -.card:hover { - border-color: var(--ifm-color-primary); - transform: translateY(-2px); - background: var(--ifm-color-emphasis-100); -} - -.cardFeatured { - composes: card; - border-color: var(--ifm-color-primary-lighter); - background: var(--ifm-color-emphasis-100); -} - -.meta { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; -} - -.time { - font-size: 0.8rem; - font-weight: 500; - color: var(--ifm-color-emphasis-600); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.badge { - font-size: 0.65rem; + font-size: 2.75rem; font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - padding: 2px 8px; - border-radius: 99px; - background: var(--ifm-color-primary); - color: #fff; -} - -.title { - font-size: 1.15rem; - font-weight: 600; - margin: 0 0 0.4rem; - line-height: 1.35; -} - -.desc { - font-size: 0.88rem; - color: var(--ifm-color-emphasis-700); - line-height: 1.5; + letter-spacing: -0.03em; + line-height: 1.1; + color: #111827; margin: 0 0 0.75rem; } -.tags { - display: flex; - gap: 6px; - flex-wrap: wrap; +.heroSub { + font-size: 0.95rem; + color: #6b7280; + max-width: 540px; + line-height: 1.65; + margin: 0 0 1.25rem; } -.tag { - font-size: 0.7rem; +.hiringBtn { + display: inline-block; + background: #111827; + color: #fff !important; + font-size: 0.82rem; font-weight: 500; - padding: 2px 10px; - border-radius: 99px; - background: var(--tag-bg); - color: var(--tag-text); + padding: 0.45rem 1rem; + border-radius: 6px; + text-decoration: none !important; + transition: background 0.15s; } -:global([data-theme='dark']) .tag { - background: var(--tag-bg-dark); - color: var(--tag-text-dark); +.hiringBtn:hover { + background: #000; } -.arrow { +/* ── Marquee ──────────────────────────────────────────────────────────── */ +.marqueeWrap { + margin: 2.5rem 0 0; + padding: 1.25rem 0; + border-top: 1px solid #f3f4f6; + border-bottom: 1px solid #f3f4f6; + overflow: hidden; +} + +.marqueeLabel { + text-align: center; + font-size: 0.62rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.14em; + color: #9ca3af; + margin: 0 0 1rem; +} + +.marqueeOuter { + position: relative; + overflow: hidden; +} + +.fadeLeft { + pointer-events: none; position: absolute; - right: 1rem; - top: 50%; - transform: translateY(-50%); - color: var(--ifm-color-emphasis-400); - transition: color 0.15s, transform 0.15s; + left: 0; top: 0; bottom: 0; + width: 5rem; + background: linear-gradient(to right, var(--ifm-background-color, #fff), transparent); + z-index: 10; } -.card:hover .arrow { - color: var(--ifm-color-primary); - transform: translateY(-50%) translateX(3px); +.fadeRight { + pointer-events: none; + position: absolute; + right: 0; top: 0; bottom: 0; + width: 5rem; + background: linear-gradient(to left, var(--ifm-background-color, #fff), transparent); + z-index: 10; } +.marqueeTrack { + display: flex; + align-items: center; + white-space: nowrap; + animation: marquee 28s linear infinite; +} + +@keyframes marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} + +.marqueeItem { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0 1.4rem; + font-size: 0.82rem; + color: #4b5563; + font-weight: 500; +} + +.marqueeIcon { + flex-shrink: 0; + border-radius: 2px; +} + +.marqueeSep { + margin-left: 1.2rem; + color: #e5e7eb; + font-weight: 300; +} + +/* ── Post list ────────────────────────────────────────────────────────── */ +.list { + margin-top: 0.5rem; +} + +.post { + padding: 2.25rem 0; + border-bottom: 1px solid #f3f4f6; +} + +.titleLink { + text-decoration: none !important; + color: inherit; +} + +.title { + font-size: 1.4rem; + font-weight: 600; + line-height: 1.3; + letter-spacing: -0.01em; + color: #111827; + margin: 0 0 0.5rem; + transition: color 0.12s; +} + +.titleLink:hover .title { + color: #0ea5e9; +} + +.desc { + font-size: 0.875rem; + color: #6b7280; + line-height: 1.55; + margin: 0 0 0.6rem; +} + +.meta { + font-size: 0.82rem; + color: #6b7280; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0; +} + +.authorLink { + color: #374151; + font-weight: 500; + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: #d1d5db; +} + +.authorLink:hover { + color: #0ea5e9; + text-decoration-color: #0ea5e9; +} + +.authorName { + color: #374151; + font-weight: 500; +} + +.authorSep { + margin: 0 0.3rem; + color: #d1d5db; +} + +.metaDash { + margin: 0 0.35rem; + color: #d1d5db; +} + +.date { + color: #9ca3af; +} + +/* ── Pagination ───────────────────────────────────────────────────────── */ .pagination { - max-width: 960px; - margin: 0 auto; - padding: 1rem 1.5rem 3rem; + padding: 1.5rem 0 4rem; display: flex; justify-content: space-between; } -.paginationLink { - font-size: 0.9rem; +.pageLink { + font-size: 0.85rem; font-weight: 500; - color: var(--ifm-color-primary); + color: #374151; text-decoration: none; } -.paginationLink:hover { - text-decoration: underline; +.pageLink:hover { + color: #0ea5e9; } -@media (min-width: 640px) { - .grid { - grid-template-columns: repeat(2, 1fr); - } - - .grid .cardLink:first-child { - grid-column: 1 / -1; - } - - .grid .cardLink:last-child:nth-child(even) { - grid-column: 1 / -1; - } +/* ── Dark mode ────────────────────────────────────────────────────────── */ +[data-theme='dark'] .heroTitle, +[data-theme='dark'] .title { + color: #f9fafb; +} + +[data-theme='dark'] .heroSub, +[data-theme='dark'] .desc, +[data-theme='dark'] .date { + color: #9ca3af; +} + +[data-theme='dark'] .post, +[data-theme='dark'] .marqueeWrap { + border-color: #1f2937; +} + +[data-theme='dark'] .authorLink, +[data-theme='dark'] .authorName { + color: #e5e7eb; +} + +[data-theme='dark'] .hiringBtn { + background: #f9fafb; + color: #111827 !important; +} + +[data-theme='dark'] .hiringBtn:hover { + background: #fff; } From 8e616ecdf4726134c9562bd395355d6e62005583 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 11 Apr 2026 18:02:56 -0700 Subject: [PATCH 4/5] add BlogPostPage swizzle: hide sidebar, add hiring CTA on every post --- .../src/theme/BlogPostPage/index.js | 40 +++++++++++ .../src/theme/BlogPostPage/styles.module.css | 68 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 docs/my-website/src/theme/BlogPostPage/index.js create mode 100644 docs/my-website/src/theme/BlogPostPage/styles.module.css diff --git a/docs/my-website/src/theme/BlogPostPage/index.js b/docs/my-website/src/theme/BlogPostPage/index.js new file mode 100644 index 00000000000..ab8a8a173a8 --- /dev/null +++ b/docs/my-website/src/theme/BlogPostPage/index.js @@ -0,0 +1,40 @@ +import React, {useEffect} from 'react'; +import OriginalBlogPostPage from '@theme-original/BlogPostPage'; +import styles from './styles.module.css'; + +function HiringCTA() { + return ( +
+
+

We're hiring

+ + Like what you see? Join us + + +

Come build the future of AI infrastructure.

+
+
+ ); +} + +export default function BlogPostPage(props) { + // Add body class so CSS can hide the sidebar + useEffect(() => { + document.body.classList.add('blog-post-body'); + return () => document.body.classList.remove('blog-post-body'); + }, []); + + return ( + <> + + + + ); +} diff --git a/docs/my-website/src/theme/BlogPostPage/styles.module.css b/docs/my-website/src/theme/BlogPostPage/styles.module.css new file mode 100644 index 00000000000..b9a0a479eba --- /dev/null +++ b/docs/my-website/src/theme/BlogPostPage/styles.module.css @@ -0,0 +1,68 @@ +.ctaOuter { + max-width: 820px; + margin: 0 auto; + padding: 0 2rem 4rem; +} + +.cta { + border-radius: 16px; + background: #f9fafb; + border: 1px solid #e5e7eb; + padding: 2.5rem 2rem; + text-align: center; +} + +.ctaEyebrow { + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: #9ca3af; + margin: 0 0 0.75rem; +} + +.ctaLink { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.01em; + color: #111827; + text-decoration: none !important; + transition: color 0.15s; +} + +.ctaLink:hover { + color: #0ea5e9; +} + +.ctaArrow { + width: 1.25rem; + height: 1.25rem; + transition: transform 0.15s; + flex-shrink: 0; +} + +.ctaLink:hover .ctaArrow { + transform: translateX(3px); +} + +.ctaSub { + margin: 0.75rem 0 0; + font-size: 0.875rem; + color: #6b7280; +} + +[data-theme='dark'] .cta { + background: #1f2937; + border-color: #374151; +} + +[data-theme='dark'] .ctaLink { + color: #f9fafb; +} + +[data-theme='dark'] .ctaSub { + color: #9ca3af; +} From 14eed244711fab63bd353307022edb2a6496549f Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 11 Apr 2026 18:02:59 -0700 Subject: [PATCH 5/5] add redis circuit breaker blog post with React diagrams --- .../blog/redis_circuit_breaker/diagrams.js | 159 ++++++++++++++++++ .../blog/redis_circuit_breaker/index.md | 97 +++++++++++ 2 files changed, 256 insertions(+) create mode 100644 docs/my-website/blog/redis_circuit_breaker/diagrams.js create mode 100644 docs/my-website/blog/redis_circuit_breaker/index.md diff --git a/docs/my-website/blog/redis_circuit_breaker/diagrams.js b/docs/my-website/blog/redis_circuit_breaker/diagrams.js new file mode 100644 index 00000000000..8fd1550738b --- /dev/null +++ b/docs/my-website/blog/redis_circuit_breaker/diagrams.js @@ -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'}) => ( + + + + +); + +export function CascadeFailure() { + return ( +
+
+

Without circuit breaker — cascade failure

+
+
LiteLLM Pod (×100)
+ +
Rate limit / cache check
+
+ + hangs 30s per request +
+
Redis — degraded, timing out
+ +
Postgres — 100× normal read load
+ +
Total outage — gateway down
+
+
+
Slow Redis → every auth check times out → database overwhelmed → full cascade
+
+ ); +} + +export function CircuitBreakerStates() { + const circle = (border, color, label, sub) => ( +
+
+ {label} + {sub} +
+

{'\u00a0'}

+
+ ); + const arrow = (label) => ( +
+ {label} +
+
+ +
+
+ ); + return ( +
+
+

Circuit breaker state machine

+
+ {circle('#1f2937','#111827','CLOSED','normal')} + {arrow('5 failures')} + {circle('#f87171','#dc2626','OPEN','fast-fail')} + {arrow('60s timeout')} + {circle('#fbbf24','#b45309','HALF-OPEN','probing')} +
+
+
+
+ +
+
+ probe success → CLOSED +
+
+
+ +
+
+ probe failure → OPEN again +
+
+
+
+ ); +} + +export function CircuitBreakerFlow() { + return ( +
+
+

With circuit breaker — graceful degradation

+
+
Incoming request
+ +
Circuit Breaker
+
+
+ + Closed +
Redis call
normal latency
+
+
+ + Open +
Fast-fail — 0ms
no network call
+ +
DB fallback
bounded load
+
+
+
Request completes — gateway stays up
+
+
+
Redis down → circuit opens → 0ms rejection → DB absorbs bounded fallback traffic
+
+ ); +} + +export function IncidentTimeline() { + const row = (color, text) => ( +
+
+

{text}

+
+ ); + return ( +
+
+

Redis degrades — before vs. after

+
+
+

Without circuit breaker

+ {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')} +
+
+

With circuit breaker

+ {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')} +
+
+
+
+ ); +} diff --git a/docs/my-website/blog/redis_circuit_breaker/index.md b/docs/my-website/blog/redis_circuit_breaker/index.md new file mode 100644 index 00000000000..3001f4278ed --- /dev/null +++ b/docs/my-website/blog/redis_circuit_breaker/index.md @@ -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 + + + +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. + + + +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 + + + +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 + + + +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.