Backend: Cache localized GetLanguages/GetRegions responses per locale#9302
Conversation
Since #9173, GetLanguages localized all ~300 language names on every request via babel.Locale.parse, costing ~38ms of CPU per call. The responses are static per locale, so cache the built protos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MobileInstall the Dev Tool (iOS via TestFlight, iOS Simulator, or Android .apk) here. Scan the QR with your phone camera, or tap Open in Dev Tool on the device, to open this branch in the installed Dev Tool dev client.
Deep linksiOS Android Web (Vercel)View the Vercel web preview for this branch. Backend
Other
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 Release NotesThis PR does not need to be included in release notes. Reason: This is a backend performance optimization that fixes CPU usage on resource endpoints, but it does not introduce a user-visible feature or a noticeable end-user change on its own. While important operationally, it is closer to internal infrastructure/performance maintenance than a major platform-facing improvement users would recognize in release notes. 🤖 Bot Debug InformationModel: |


Since #9173,
Resources/GetLanguageslocalizes all ~300 language names on every request. Each name goes throughbabel.Locale.parse, and ~120 of our ISO639-3 codes aren't known to babel at all — for those, babel does an uncached filesystem stat and raisesUnknownLocaleErroreach time. That adds up to ~38ms of pure CPU per request (measured warm), on an endpoint that used to be a cached-dict lookup — which is what caused the backend CPU usage spike starting 2026-06-28.The responses are fully static per locale, so this caches the built response protos in an
lru_cachekeyed on the babel locale (bounded by the set of supported UI locales). Per-request cost returns to effectively zero; the first request per locale after startup pays the localization cost once.GetRegionsgets the same treatment (it was cheap, but it's the same shape).Sharing one proto message across requests is safe here: nothing mutates responses after the servicer returns them — the tracing interceptor's sensitive-field sanitization deepcopies first — and there's a comment in the servicer noting this caveat.
Also removes the now-unused per-code
try_localize_*wrappers fromLocalizationContext, since the servicer now calls the module-level functions fromcouchers/i18n/localize.pydirectly (those remain and are still tested).Testing
GetLanguagescall (~24ms of that in the exception path for the 122 codes babel doesn't know); the new path is anlru_cachehit after the first call per locale.uv run pytest src/tests/test_resources.py src/tests/test_localize.py— 13 passed.test_resources.pyalready covers localized language/region names for non-English locales, which exercises the cached path.make formatandmake mypypass.Backend checklist
test_resources.py/test_localize.py)developif necessary for linear migration history (no database changes)For maintainers
This PR was created with the Couchers PR skill.