A reverse-engineering deep dive into why GTA Online took six minutes to load — and how two lines of quadratic code and a misunderstood libc function were to blame.
7 min read
GTA Online was infamous for multi-minute loads long after launch. In February 2021, an independent reverse-engineering writeup (How I cut GTA Online loading times by 70%) showed why: not disk, not the peer-to-peer session, but a single-threaded CPU stall while parsing a huge JSON shop catalog — with a quadratic duplicate check on top.
This ShopperCove post is a clean, hands-on-toned walkthrough of that public writeup. It is not a ShopperCove lab on our box. Every timing below is the original author's measurement on their hardware unless labeled otherwise. Rockstar later shipped a fix; we cite that update honestly.
Related links:
Attribution bar: Source author hardware — AMD FX-8350, Kingston SA400S37120G SSD, 16 GB DDR3-1337, GTX 1070. Measurement window: Rockstar logo → in-game, startup menu disabled, Social Club login excluded. Affiliates: 0.
On that machine, story mode loaded in about 1 minute 10 seconds. Online mode took roughly 6 minutes flat — about 5× longer on the same hardware. A Reddit poll cited in the writeup suggested more than 80% of respondents were annoyed by load times; high-end PCs in community benchmarks still sat around ~2 minutes online — better, but not close to story-mode parity.
Community workarounds (skip logo mods, story-then-online tricks) were described as saving only 10–30 seconds combined, if anything measurable. The author could not measure a difference from the story-to-online technique.
Task Manager was enough for the first pass. After about a minute loading resources shared with story mode, online load pegged a single CPU core for ~four minutes while doing essentially nothing else:
That ruled out I/O, network negotiation, and GPU work. The FX-8350's weaker single-thread performance could explain some of the gap versus faster CPUs — but a core stuck at 100% for minutes pointed at bad code, not "the game is sophisticated."
For closed-source software, the author used stack sampling (Luke Stackwalker) rather than an instrumenting profiler. Without debug symbols, matching addresses to the same function was done by eye. Two hot spots showed up, not one.
De-obfuscating a memory dump (Process Dump; the executable was obfuscated) and stepping with x64dbg revealed what was being parsed: JSON — about 10 MB with roughly 63,000 item entries for a "net shop catalog" of purchasable items and upgrades.
Example catalog entry shape (from the writeup):
One hot path labeled as strlen, with vscan_fn and what looked like sscanf further down the stack. Many sscanf implementations call strlen internally to build an internal FILE-like object. Parsing values out of a large JSON blob with sscanf therefore re-scans the length of the remaining string on every call — turning "linear-looking" parsing into something far worse on a 10 MB buffer.
Right next to the parser, in the same if, each parsed item was inserted into an array of {hash, item} pairs — but only after a full linear scan comparing hashes to see if the item already existed.
With ~63,000 entries that is (n² + n) / 2 comparisons — about 1,984,531,500 by the author's math. The array started empty and every JSON key was unique, so the duplicate check was pointless. A direct-insert path already existed and could have been used instead.
To prove the diagnosis, the author injected a DLL and hooked the hot paths (minhook). Full PoC source is linked from the original writeup on GitHub.
strlen path: cache start/end of a long string the first time its length is computed; return the cached length when the pointer falls inside that range; disable the hook once the remaining range shrinks below a threshold.
Catalog insert path: skip the duplicate check and insert directly (keys known unique):
ShopperCove did not re-run this PoC. Treat it as the author's evidence package, not a recommendation to inject third-party DLLs into a live multiplayer client.
From the original results table (same hardware, same measurement method):
| Condition | Online load (author) |
|---|---|
| Original | ~6m flat |
| Duplication-check patch only | 4m 30s |
| JSON / strlen patch only | 2m 50s |
| Both patches | 1m 50s |
Author math: (6×60 − 110) / (6×60) ≈ **69.4%** reduction — the "70%" in the title.
Not ShopperCove lab numbers. Other machines may have different bottlenecks after the catalog path is fixed.
Before the real bottleneck was found, the community had already tried:
Task Manager alone was enough to rule disk and network out during the multi-minute stall. The deeper lesson for operators and game engineers is the same one we keep hitting in server posts: profile before you assume, and beware accidentally quadratic code that looks fine on small test data and falls over on production-sized inputs.
Related links:
Per the original author's updates (March 2021):
ShopperCove has not independently re-benchmarked current GTA Online builds on that 2013-era CPU setup.
No. It is a cleaned rewrite of a public reverse-engineering post so the live CMS page stops showing raw markdown dumps and placeholders. Timings belong to the original author.
Do not treat this post as a how-to for online multiplayer clients. Rockstar shipped an official fix. Third-party injection risks bans and worse. The engineering takeaway is the profiling story.
Same failure mode: averages and vibes hide quadratic or single-thread stalls. Measure the hot path.
The writeup's diagnosis is clear and well evidenced by the author: online load spent minutes on a single core parsing ~10 MB of catalog JSON via an sscanf/strlen pattern, then inserted ~63k unique items with a full-array duplicate scan. Author-measured patches dropped ~6m → ~1m 50s (~69%). Rockstar later fixed it. ShopperCove's job here is honest attribution and a CMS-clean body — not invented lab numbers.
Affiliates: 0.
webpack-bundle-analyzer 5.4.0 vs source-map-explorer 2.5.3 on one webpack 5.111 production build (148,569 B): date-fns 44,313 B vs 19,196 B (2.3x) because of module concatenation estimates; lodash 70,136 vs 70,312 B. CLI medians 313 ms vs 224 ms (JSON).
Hands-on5 Oct 20265 min read
Both cut a string at a separator, but they fail in different ways. We ran them on Python 3.13.5 to show where partition() is safer and where split() is the right tool.
Summary2 Nov 20263 min read
What changes when you swap sort() for toSorted(), with a real 1-million-item Node.js benchmark and the support table.
Summary1 Nov 20263 min read