The live tick was storing every synced row in Redis, sixty times an hour - #78
Merged
Merged
Conversation
Redis-g3Yk filled its 50GB volume and stopped being able to save at all.
The dataset was 8.65GB on disk and 25.3GB in memory across 209 keys, and
two of those keys were 23.5GB of it:
bull:live-scores:events 15.1GB ~1.5MB per entry
bull:live-plays:events 8.4GB ~844KB per entry
What a BullMQ processor returns is written twice, into the job hash and
into the queue's `completed` event. Since the mirror landed (#68, #69)
both live pollers handed back `mirror: out` -- the whole nichedb payload,
every row they had just synced -- next to the counts they already
reported. Nothing reads `mirror`, and nothing reads `returnvalue` at all.
At one tick a minute and BullMQ's default ceiling of 10,000 events, that
is ten thousand copies of a sync.
The disk then failed in the usual spiral: `--save 60 1` writes a full
second copy of the dataset to `temp-<pid>.rdb` before renaming it over
dump.rdb, an 8.65GB write that no longer fit, so every save died partway
and left its partial file behind. Eleven of them, 30GB, going back to the
day after the mirror shipped -- each one eating the space the next save
needed. Redis crash-looped: load for 124s, accept connections, start a
save, get killed.
So: return the counts and not the payload. The counts were already in the
object, so no caller changes.
The second half is a ceiling that does not depend on nobody ever
returning something big again. BullMQ's 10,000 is a sensible count and a
dangerous size, because it costs 10,000 times whatever the fattest job
returns. These streams are for observability and nothing here reads them
back, so every queue and worker now caps them at 1,000 -- which turns the
same mistake into a slow query instead of an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 22, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Redis-g3Yk filled its 50GB volume and stopped being able to save at all, which took
tipoffwatch.com down: the container crash-looped every 2¼ minutes, loading for 124s,
accepting connections, starting a background save and getting killed.
Why it filled
The dataset was 8.65GB on disk and 25.3GB in memory across 209 keys. Two of those
keys were 23.5GB of it:
bull:live-scores:eventsbull:live-plays:eventsWhat a BullMQ processor returns is written twice, into the job hash and into the queue's
completedevent. Since the mirror landed (#68, #69 on 2026-09-10) both live pollersreturned
mirror: out— the whole nichedb payload, every row they had just synced —alongside the counts they already reported. Nothing reads
mirror, and nothing readsreturnvalueanywhere in the repo. At one tick a minute against BullMQ's default ceilingof 10,000 events, that is ten thousand copies of a sync.
The disk then failed in the usual spiral.
--save 60 1writes a full second copy of thedataset to
temp-<pid>.rdbbefore renaming it overdump.rdb; that 8.65GB write nolonger fit, so every save died partway and left its partial file behind. Eleven of them,
30GB, the oldest dated the day after the mirror shipped — each one eating the space the
next save needed.
What this changes
syncLiveScores/syncPlaysreturn counts, not the payload. The counts werealready in the object, so no caller changes.
sensible count and a dangerous size: it costs 10,000× whatever the fattest job returns.
These streams are observability only and nothing here reads them back, so the smaller
ceiling turns the same mistake into a slow query rather than an outage.
Already done on the Railway service
Not in this diff, for the record:
temp-*.rdbfiles —/datawent 36G/79% → 8.1G/18%dump.rdb8.65GB → 1.80GB,RDB load 124.5s → 25.9s
temp-*.rdbat boot and saves on900 100instead of60 1, so a snapshot is not rewriting the whole dataset every minuteDB saved on disksince Sep 11; verified live withx-cache: missthenhitTesting
The seven test files that touch the changed surface (
mirror,syncLiveScores,syncPlays,startWorkers,@tipoff/queue) were run againstorigin/main's version ofthe three files and then against this branch, same command, back to back:
The full
bun testsuite is not deterministic in a local shell and should not be read asa gate here: it reports 3 or 4 failures depending on the run, and the test count varies
(1567 vs 1554).
robots-auth-paths×2 andplaylist-editing×1 fail identically on aclean
main;live-feed.test.jsneedsDATABASE_URL, makes real ESPN requests throughthe bandwidth-capped proxy, and times out at module load under CPU contention. None of
them import
@tipoff/queueor call the changed functions. The repo has no.github/workflows, so Socket Security is the only CI gate.biome checkreports the same 2 pre-existing warnings asmainand no new ones.Worth a separate look
The app never reconnects after a Redis restart. Each time Redis bounced,
/healthzkeptanswering 200 while
/hung indefinitely, and only a redeploy of the web service broughtit back — the same signature as the 2026-09-13 outage. The db-watchdog from genrewatch
PR #23 is still unported, and it is what turns any Redis restart into a silent outage.
🤖 Generated with Claude Code