Symbolication reads every debug file in the project for each event that has no native debug_id
We self-host GlitchTip for a Flutter app that ships on Windows, macOS, iOS and Android, and CI uploads debug symbols on every build. After a few months the project had about 13 GB of debug files, and the box started to fall over: web worker pinned at 100% CPU, then OOM, and once it took the whole server down with it. It turned out that every event without a native debug_id makes the resolver read every debug file in the project. Details and a workaround are below.
AI-assisted technical details:
Summary
event_difs_resolve_stacktrace (apps/difs/tasks.py) only narrows the DIF query by debug_id when the event carries native debug images:
native_debug_ids = _extract_native_debug_ids(event)
if native_debug_ids:
difs = difs.filter(data__debug_id__in=native_debug_ids)When the list is empty, the loop runs over every non-proguard DIF in the project. For each one it copies the blob to a temp file (difs_concat_file_blobs_to_disk) and runs the native resolver. This happens synchronously in the ingest path.
Present in 6.2.0, 6.2.6 and current master (the filter above is the same on master today).
Which events hit it
- Any non-Android exception event with no
instruction_addrframes, e.g. a plain Dart exception from Flutter desktop/iOS. It goes into theelsebranch, finds no native debug ids, and scans everything, even though there are no addresses to resolve. - Native events whose images are not
NativeDebugImage. On 6.2.0 that included Windowspeimages (logged asUnknown debug image type: pe). 6.2.6 now acceptspe, which helps, but the first case is still there.
What we saw
Self-hosted, one project with a Flutter app (Windows, macOS, iOS, Android) that uploads symbols from CI on every build:
| DIF rows in the project | 4352 |
| distinct debug_ids | 1130 (CI re-uploads the same symbols, so most rows are duplicates) |
| total DIF size | 13 GB |
| web container | 2 vCPU pegged, ~95% system time, /code/uploads/file_blobs/* open in a loop |
| result | memcg OOM kills of the worker with a 1.5 GB limit; on an earlier occasion the host OOM took the whole box down |
After the local patch below: web CPU 191% → ~1%, memory 1.5 GB → ~240 MB, no OOM kills, and ingest keeps working.
Local workaround we run
if native_debug_ids:
difs = difs.filter(data__debug_id__in=native_debug_ids)
else:
difs = difs.none()This drops the "try every file" fallback completely, so it also drops whatever that fallback resolves for events without debug_meta (e.g. obfuscated Flutter Android). It is fine for us, but probably not the right upstream fix.
Suggested fix
- Skip native resolution when the event has no native frames. There is nothing to resolve, so there is no reason to touch any DIF.
- When there are native frames but no debug ids, bound the fallback: cap it to N files (newest per distinct debug_id, matching arch), or put it behind a setting.
- Optionally dedupe by debug_id so re-uploaded symbols do not multiply the work.
Happy to open an MR for 1 and 2 if that direction sounds right.
AI disclosure: Claude Code investigated the incident and drafted the technical details. Codex entered the issue text in GitLab. The reporter wrote the opening summary and reviewed the technical draft.