Android: `external` memory accounting climbs to ~3.4 GB and aborts Hermes after ~30s, while real memory stays flat (regression after 2.1.1)

Author: friedolinfoerderCreated Aug 11, 2026Updated Aug 24, 2026

Summary

On Android, our app aborts deterministically ~30 seconds after launch with a Hermes Max heap size was exceeded OOM. The JS heap is nearly empty at that moment (17.6 MB allocated) and process RSS/PSS is flat, but Hermes reports external = 3,433,173,207 (~3.4 GB) of native memory registered against the heap.

This looks like an over-reporting bug in the external memory pressure accounting rather than an actual leak: no real memory is being consumed. TOTAL PSS is flat and even falls over the sampling window right up to the abort.

The app is stable on 2.1.1 and dies on every version we have tried from 2.2.12 onward, including 2.6.2 — which already contains #3495, #3709 and #3803.

Environment

@shopify/react-native-skia 2.6.2 (also reproduced on 2.2.12; 2.1.1 unaffected)
react-native 0.85.3
Expo SDK 56
Hermes hermes-engine 250829098.0.10 (Hermes V1)
New Architecture enabled (bridgeless — note [RNBridgeless] in the log)
Build type release (minified JS, Hermes bytecode)
Devices Android emulator API 36; originally found on a physical Pixel 9a (Android 17)
iOS not affected — same commit builds, renders and stays alive on the iOS simulator

What we see

Abort, from logcat:

I/HermesGC: OOM: [RNBridgeless] reason = Max heap size was exceeded
            (1 from category: vm_allocate_category),
            numCollections = 51,
            heapSize   =         20971520,   //  20.0 MB
            allocated  =         18436064,   //  17.6 MB
            va         =         20971520,   //  20.0 MB
            external   =       3433173207.   //   3.4 GB   <-- this
F/libc   : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
           in tid 4368 (mqt_v_js)

Process memory sampled every 10s via dumpsys meminfo over the same run — note it is flat/declining, i.e. there is no real growth to match the 3.4 GB figure:

t(s)  TOTAL_PSS(kB)  JAVA_HEAP  NATIVE_HEAP  alive
   0         379187      36420       195220  yes
  10         360398      14880       194800  yes
  20         358858      12192       193988  yes
  30              ?          ?            ?  NO -- PROCESS GONE

Version matrix

Verified which fixes each tag contains using the compare API (compare/<merge_commit>...<tag>ahead = contains):

version #3495 recorder pictures #3709 update pressure after dispose #3803 canvas texture pressure result
2.1.1 stable, ran >160s, PSS flat
2.2.12 OOM at ~31s, 2/2 launches
2.6.2 OOM at ~30s

Intermediate 2.4.x releases were also affected in our testing.

We checked 2.9.1 as well before filing: the only change to JsiSkHostObjects.h in 2.6.2...2.9.1 is an include-path refactor, and JsiSkImage.h only gains a Graphite readback path — so there is no reason to expect the current release to behave differently, and we did not test it.

What we ruled out

  • Not an actual leak. PSS, Java heap and native heap are all flat or falling (see trace above). Only the external counter grows.
  • Not the SurfaceTexture log noise. We see updateAndRelease: EGLConsumer is not attached to an OpenGL ES context (42 occurrences in the 30s window), but this is present on the working 2.1.1 build too, and in fact more frequently there (1595 in 60s vs 243 in 40s when we measured it). It does not correlate with the abort.
  • Not the three prior memory-pressure fixes. All of #3495, #3709 and #3803 are ancestors of the 2.6.2 tag we tested.

Our usage

The screen that triggers this is our app's logged-out welcome screen, whose only Skia content is a continuously animating Skottie canvas (a looping bot animation). Elsewhere the app uses Canvas, Image, ImageSVG, Mask, Group, Paint, shaders/gradients and useClock.

Because the animation runs continuously from launch, the abort time is very consistent (~30s), which suggests the counter grows at a fixed rate per frame rather than in response to any user interaction.

Reproduction

We do not yet have a minimal reproducer — this is currently only reproduced in our application, which we cannot share. Given that the trigger appears to be a continuously-animating Skottie canvas in a release build under Hermes V1 + bridgeless on Android, we would expect a bare RN app looping a Skottie animation to show the same external growth, and we are happy to attempt that reproducer if it would help.

We are also happy to run instrumented builds or patches against our app, since the repro here is deterministic and takes 30 seconds.

Impact

We are pinned to 2.1.1 and cannot take Expo SDK 56's bundled 2.6.2, so we are holding back roughly a year of Skia releases.

Source: Shopify/react-native-skia