I track what I watch. My backlog grew past 40 titles and I had no idea what
that actually meant in hours. Every tracker (MyAnimeList, AniList, Shikimori)
shows you a list — none of them multiply it out into something you can
actually plan your week around.
So I built TokiWa — Next.js 15, Prisma, Postgres on
Neon. The core feature is dumb-simple: remaining_episodes × episode_duration,
shown as real hours, with a "what fits in an evening / a weekend / a
vacation" breakdown. Nobody in the anime-tracker space does this. Turns out
general TV trackers like MyShows had something similar years ago — readers
on a post I wrote pointed that out, which was a good reminder that "unique"
claims deserve a footnote.
Here's what actually went wrong while building it.
Postgres thinks NULL is the biggest number there is
Sorting the catalog by score, descending:
\ts
orderBy: { score: "desc" }
\\
Titles with no score yet (unreleased anime) floated to the very top of the
homepage, ahead of a 9.1-rated classic. Postgres puts NULLs first on a
DESC sort by default — I'd never hit this because every other sortable
field I'd used was NOT NULL. Fix:
\ts
orderBy: { score: { sort: "desc", nulls: "last" } }
\\
Same bug had picked the homepage hero title too, via a separate query.
Grepped for every orderBy: { score\ in the codebase to be sure I got them
all.
A sitemap can be too big to deploy
Catalog grew to 15k+ titles. Sitemap grew with it — to 24MB. Vercel caps
prerendered static files at 19MB. Every deploy after that point failed
silently in a way that took a minute to diagnose. Split it into shards of
2000 titles each via Next's generateSitemaps()\:
\ts
export async function generateSitemaps() {
const count = await prisma.title.count();
const shards = 1 + Math.ceil(count / TITLES_PER_SITEMAP);
return Array.from({ length: shards }, (_, id) => ({ id }));
}
\\
The sitemap index at /sitemap.xml\ still resolves — search consoles never
needed re-submitting.
Bulk-syncing 600 pages of data exhausted the connection pool
Backfilling the catalog from an external API meant one query per title to
check if it already existed — fine at small scale, catastrophic at 30,000
candidate titles over several hours. Neon's pool has a hard connection
limit; a few hours in, everything started failing with:
\
Timed out fetching a new connection from the connection pool.
\\
The fix wasn't more connections — it was fewer round trips. One
findMany({ where: { malId: { in: ids } } })\ per batch of 50 instead of
50 individual findUnique\ calls. Also added a retry wrapper so one flaky
page doesn't kill a multi-hour run:
\ts
async function withRetry<T>(action: () => Promise<T>, attempts = 4) {
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await action();
} catch (error) {
if (attempt === attempts) return null;
await new Promise((r) => setTimeout(r, attempt * 3000));
}
}
}
\\
The data source silently started redirecting every request
One of the external APIs I depend on switched its canonical domain and
began 301-redirecting every single call. fetch()\ follows redirects
silently, so nothing broke — it just got slower, one wasted round trip
per request, invisible until I actually looked at response headers.
Pointed the client straight at the new domain and moved on.
TokiWa is free, no forced signup to browse, no ads. Catalog re-syncs
itself nightly now via a cron job so it doesn't need me babysitting it.
If you poke around and find something broken — the catalog is big enough
that edge cases are definitely still hiding somewhere — I'd genuinely like
to hear about it.





Top comments (0)