---
title: "Stories Get"
method: GET
path: "/v1/stories/{slug}"
---

# Stories Get

`GET /v1/stories/{slug}`

## Request

- Base URL: `https://api.storyflo.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.storyflo.com/v1/stories/{slug}`
- Auth: none declared

Return a single story by slug. Resolves real DB rows first, then
falls back to the demo set — so the public ``/story/[slug]`` page on
the frontend works for both onboarded publishers' articles and the
curated demo content. When the article has a completed render-job,
``audio_url`` is populated so the frontend player can stream the
pre-rendered audio without paying for a second inference call.

Perf 5-17: a single story is effectively immutable for the cache
window — body_text + audio_url only flip when a render completes or
an operator re-curates. 60s shared cache + SWR absorbs the bursty
"10 listeners open the same share link" pattern (e.g., social shares)
without hitting Fly for every fetch. The frontend story page was
cold-fetching this endpoint on every nav and paying 18s on cold
start; the edge cache eliminates that path entirely after the first
visitor.

Pool-safety (2026-08-11 SEV) — TWO PHASES, ONE CONNECTION AT A TIME
==================================================================

This handler used to carry ``db: AsyncSession = Depends(get_db)``.
That bound a PRIMARY connection to the whole request while phase 1
below independently checked out a REPLICA connection — so every
``/v1/stories/{slug}`` hit pinned **two** pooled connections at
once, and held the primary one completely idle across the expensive
replica read AND across the synchronous boto3 R2 ``head_object``
round-trips ``get_story_full`` issues per audio variant
(``app/search.py`` ``has_audio``).

On 2026-08-11 a ~3.4x burst on this endpoint (713 requests in five
minutes, almost all ``daily-brief-*``) drove the single app task to
99% CPU; with the event loop starved, every parked coroutine kept
its connections, and BOTH pools hit their 15+5 ceiling within
~90 seconds. Twelve endpoints returned
``QueuePool limit of size 15 overflow 5 reached``.

Fix: run the expensive read holding ZERO primary connections, then
open a short-lived primary session for the cheap point-lookups and
release it in a ``finally``. Peak concurrent connections per
request drops from 2 to 1, and the primary is never held across R2.

## Path parameters

- `slug` string, required

## Headers

- `x-listener-token` string, nullable
- `x-storyflo-email-token` string, nullable

## Response `200`

Successful Response

- object

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/storyflo/apis/storyflo-inference-router.md) · [All operations](https://skmtc.dev/storyflo/apis/storyflo-inference-router/llms.txt) · [OpenAPI document](https://skmtc.dev/storyflo/apis/storyflo-inference-router/revisions/c3a1a735ac7a?raw)
