Scraped to death: the 2026-09-26 forums outage #5
DanVanAtta-bot
started this conversation in
General
Replies: 1 comment
|
Couple bug reports sent to NodeBB:
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
On Saturday 2026-09-26 the forum was slow or down from about 14:50 to 16:14 UTC. A crawler sent about 70 requests a second from ~27,000 rotating IP addresses, scraping the whole forum. Call it a free load test: it found a database index we never had. What brought the site back is that the crawler stopped, or at least slowed down a lot. By then we had a set of fixes ready, and they're all deployed now.
What happened
We migrated the forum from MongoDB to Postgres. NodeBB converted the data itself, but it only creates its database indexes on a fresh install, so our migrated database never got them. The one that mattered here is on
expireAtin thelegacy_objecttable. That table has about 450,000 rows, one for every "thing" on the forum.NodeBB's Postgres support is really a Redis-style key store built on top of Postgres. Before every write, it deletes expired rows from
legacy_object, which means readingexpireAt, the column with no index. So every write came with a full scan of 450k rows.We also had guest visitor counting turned on. To count unique guests, NodeBB records a visitor's IP address the first time it sees it each hour, and that's a write.
Put those together:
legacy_objecta minute, roughly 3 a second.Three writes a second doesn't sound like much, but each one read all 450k rows, on a server with one CPU. That pegged it at 100%. The rest of the traffic queued up behind the scans, nginx ran out of connection slots, and visitors got hard errors.
To make it worse, every request for assets (JS, CSS, fonts) also hit the database. Each one was treated as an existing guest session, and NodeBB updates a session's last-activity time on every request.
Timeline (UTC)
Why the crawler slowed the moment nginx went off, we don't know. Its earlier bursts also stopped on their own after 1.5–2.4 hours.
What we changed
expireAt, so that delete is no longer expensive. Full scans of the table went from about 170 a minute to 4 in 9 minutes. The forum now creates its indexes on every start, so a migrated or restored database can't run without them again.All reactions