Lambda renders repeat stale content as height/4 strips under full-width overlays (repro repo)
Lambda: frame content repeated as stale horizontal strips (viewport-width × height/4); two minimal repros attached
Summary
On Remotion Lambda, some frames come back with the frame's own content drawn a second time as stale horizontal strips, offset by exactly height / 4 device pixels (540 px at 4K, 270 px at 1080p). It hits a handful of frames per composition, always while something is animating, and it is deterministic across runs. We first saw it after upgrading our function from 4.0.372 to 4.0.428 (Lambda Chrome 133 → 144) and it is still present on 4.0.525 (Chrome 149); we haven't established that the browser version is the cause, it is the change that coincides with the first reports.
The same composition renders clean with renderMedia() on macOS, in Studio and in the <Player>. It only happens on Lambda's software GL stack (SwANGLE).
Two independent overlay shapes trigger it, both full viewport width, and both only when path-heavy inline SVG is being re-rasterized underneath (see "What mattered while minimizing"):
- an overlay with a repeating gradient and a fractional CSS
opacity(linear-gradientgrid atopacity: 0.5); - an overlay with a repeating
radial-gradientdot pattern and no opacity at all.
Giving that element its own compositor layer (will-change: opacity) removes the artifact in both cases at no measurable render-time cost. We ship that as a workaround; this report is so the runtime can be looked at.
Environment
Remotion / @remotion/lambda |
4.0.525 (first seen on 4.0.428) |
Lambda Chrome (per docs/lambda/runtime and the @remotion/lambda package) |
149.0.7790.0 on 4.0.525; 144.0.7559.20 on 4.0.428; 133.0.6943.141 on the 4.0.372 we ran before |
| Function | mem 4096 MB, disk 10240 MB, timeout 360 s, us-east-1, default gl |
| Render | renderMediaOnLambda, codec h264, imageFormat jpeg q90, colorSpace bt709, framesPerLambda 80, scale 2 (3840×2160). Also reproduces at scale 1. |
| Composition | 1920×1080, 30 fps, only remotion + react |
| Clean on | renderMedia on macOS 15 (same bundle), Studio, Player |
| Not seen on | 4.0.351 → 4.0.372 (Lambda Chrome 133) across a year of daily renders of this kind of content |
Reproduction
Repository with a fresh Remotion project pinned to 4.0.525: https://github.com/Rameshv/remotion-lambda-strip-ghost
src/LambdaStripGhost.tsx (249 lines) is the whole reproduction: two compositions, only remotion/react imports, inline styles, system font, a few SVG icon paths embedded (Phosphor Icons, MIT). Same file attached here: LambdaStripGhost.tsx.
git clone https://github.com/Rameshv/remotion-lambda-strip-ghost && cd remotion-lambda-strip-ghost
npm install
npm run lambda:function # 4096 MB / 10240 MB / 360 s, where we saw it; defaults reproduce as well
npm run lambda:site # deploys src/index.ts as site "lambda-strip-ghost"
npm run lambda:case1 # OpacityOverlayGhost, scale 2 -> strips at frames 111-130 (111-136 at scale 1)
npm run lambda:case2 # GradientPatternGhost, scale 2 -> strips from frame 286 to the end
npm run lambda:case1:workaround # same render with --props='{"workaround":true}' -> clean
npm run lambda:case2:workaround # clean
npm run render:local:case1 # clean on a desktop machine
Verified end to end from this repository as-is (default remotion.config.ts, no Tailwind, no custom webpack): npm run lambda:site followed by the two render scripts reproduces at exactly the frames above.
Each composition takes one prop, workaround (default false). With true it adds willChange: 'opacity' to the trigger overlay and nothing else. The trigger element is marked THE TRIGGER in the source. The render settings behind the scripts are --codec=h264 --image-format=jpeg --jpeg-quality=90 --color-space=bt709 --frames-per-lambda=80.
What the compositions contain
- Case 1: white background, full-viewport
linear-gradient60 px grid atopacity: 0.5, a title block, an SVG with two connectors drawn viastrokeDashoffset, three circular nodes with spring-scaledtransform,box-shadowand an SVG icon inside, plus labels fading in. - Case 2: white background, full-viewport
radial-gradient2.5 px dots at 48 px spacing with no opacity, a title with per-word fades, a stick figure with a caption, two "avoid" items with X strokes drawn viastrokeDashoffset, three items scaling in from 1.15 → 1 with SVG icons.
What mattered while minimizing
Starting from the real shots, we bisected towards this file on Lambda. Font (self-hosted vs system), Tailwind classes vs inline styles, isolation: isolate / overflow: hidden wrappers, an offscreen <svg><defs>, an extra white sheet: none of those changed the result.
The inline SVG icons did. With the overlay untouched, replacing the Phosphor <svg> icons inside the animating nodes with each of the following made the artifact disappear:
| Replacement for the icon (same size, same position) | Strips |
|---|---|
Phosphor inline <svg> path icon (original) |
yes |
empty <span> |
no |
same-size PNG <img> |
no |
same-size CSS conic-gradient disc |
no |
| same-size text glyph (64 px) | no |
A first port with simple SVG shapes (a rect and a short path) did not reproduce either; the path-heavy icons do. So the trigger is the combination of a full-viewport-width overlay that needs its own raster pass (fractional opacity, or a gradient pattern) over path-heavy inline SVG that is re-rasterized on that frame. Removing either half gives a clean render. A raster image or text in the same place does not trigger it, which points at the software path rasterization of inline SVG under such a parent rather than at raster cost in general.
Frame-level detection
Adjacent-frame PSNR of the rendered mp4 sits at 45–57 dB on clean frames and drops below 30 dB when a strip appears or disappears. That is how the frame numbers above were found; visual inspection confirms them (contact sheets attached).
What it looks like
Left: Lambda render. Right: the same frame with workaround: true.
Case 1, frame 120. Hub and branch nodes, connectors and labels are drawn twice; the copy is shifted by 540 px and carries a stale connector state.
Frames 108 to 134, every second frame. The strips appear at 111 and are gone by 130:
Full 4K frame:
Case 2, frame 292. The figure, the "AVOID" items, the caption and the labels repeat 540 px lower, and part of the dot pattern is missing.
Frames 282 to 299. The strips start at 286 and stay to the end:
Full 4K frame:
Renders (mp4, scale 1, scale 2, and scale 2 with the workaround, per case):
Bisection on the real production shots (same function, one change per row)
Case 1 (overlay = linear-gradient grid, opacity: 0.5, full viewport):
| Change | Strips |
|---|---|
| none | yes |
| overlay removed | no |
overlay kept, opacity removed (alpha baked into the gradient colour) |
no |
opacity kept, gradient replaced by a flat colour |
no |
overlay kept, will-change: opacity added |
no |
| overlay resized to 1920×1026 / ×972 / ×540 / ×270 / ×108 | yes, same frames |
| overlay resized to 1824×1026 / 1728×1080 / 1440×810 / 960×540 / 576×324 | no |
| 1920×270 overlay over the static title row instead of the animating diagram | no |
audio, box-shadows, transform: scale(), previous shot removed |
yes, unchanged |
So: full viewport width is required, height is not (108 px is enough), and it must cover content being re-rasterized.
Case 2 (overlay = radial-gradient(circle, rgba(81,43,16,0.07) 2.5px, transparent 2.5px) at 48 px, no opacity):
| Change | Strips |
|---|---|
| none | yes |
| dotted overlay removed | no |
| dots replaced by a flat colour | no |
dots replaced by a linear-gradient grid |
no |
overlay kept, will-change: transform or will-change: opacity added |
no |
visibility: hidden placeholders / transform: scale() / stray text removed |
yes, unchanged |
What we ruled out
- Function resources: memory 4096 and 10240 MB, disk 10240 MB, timeout 300/360 s — identical.
chromiumOptions.gl:swangle(default) andangleidentical;swiftshadercrashes the browser on Lambda ("Target closed");angle-eglrenders but is unusably slow.enableMultiProcessOnLinuxtrue and false — identical.scale1 vs 2 — both reproduce (period 270 vs 540 device px).- Not an encoder artifact: IDR frames carry the strips; clean segments stay clean.
Timeline
| Our Remotion | Lambda Chrome | Result |
|---|---|---|
| 4.0.351 – 4.0.372 (Sep 2025 – Aug 2026) | 133 | not seen |
| 4.0.428 (from 26 Aug 2026) | 144 | first reports within days |
| 4.0.525 (16 Sep 2026) | 149 | still reproduces |
Nothing in our composition code on this path changed in that window. The Remotion upgrade (and with it the Lambda browser, 133 → 144) is the only change that coincides with the first reports; we have not verified the older runtime against these exact repros, so we can't say whether the browser version is the cause or just the coincidence.
Why we think it's the tiled raster path
The repeat period is exactly viewport height / 4, which matches the tile size Chromium picks for viewport-sized layers under GPU rasterization (viewport width × ¼ viewport height). The trigger needs a full-viewport-width element whose group needs its own raster pass (fractional opacity, or a repeating gradient pattern) over inline SVG paths that are invalidated on that frame; a PNG, a CSS gradient or text in the same place does not trigger it. Promoting the element to its own layer takes it out of the tiled path and the artifact disappears. Consistent with a software-GL (SwiftShader) raster / tile-cache problem on the Lambda runtime; we can't see deeper from outside.
Workaround we ship
A render-only layout effect that, every frame, measures elements with an inline opacity or an inline gradient background and sets will-change: opacity on those wider than 95 % of the composition. Promoting every inline-opacity element instead produced a different, fainter ghost (root-layer SVG repeated half a frame lower), so the promotion has to stay limited to the wide elements.
Possibly related
#10420, #10337, #10670
Attachments
- Repro repository: https://github.com/Rameshv/remotion-lambda-strip-ghost (also attached: LambdaStripGhost.tsx)
- Videos:
case1-OpacityOverlayGhost-scale2.mp4,-scale1.mp4,-scale2-workaround.mp4,case2-GradientPatternGhost-scale2.mp4,-scale1.mp4,-scale2-workaround.mp4(see "What it looks like")
https://github.com/user-attachments/assets/c1101e66-feee-4ff6-a6eb-a400b1f02942
https://github.com/user-attachments/assets/1456edc7-faa9-48dc-9deb-f7635bd3ee9d
https://github.com/user-attachments/assets/82e49d0a-eb21-485f-80fa-9495fa944de9
https://github.com/user-attachments/assets/c1b0dbc5-a6b1-45f2-9ad5-dbd5e9a75778
https://github.com/user-attachments/assets/689e289e-3fae-446f-90e4-5d8a3def19b9
https://github.com/user-attachments/assets/321fcf62-240c-4548-80b0-29cb07590715
https://github.com/user-attachments/assets/cc0bdbc4-26a3-4a30-94de-8e5dbf23a048
Source: remotion-dev/remotion