← Blog

Going headless to stay alive: an AWT/JavaFX deadlock

June 3, 2026 · Adrian De Leon · javafx, debugging

Open the very first line of Editora’s main() and you’ll find something that looks out of place for a GUI app:

public static void main(String[] args) {
    System.setProperty("java.awt.headless", "true");
    // ...
}

A JavaFX app declaring itself headless? That’s the fix for one of the more maddening bugs I’ve chased: an intermittent macOS hang that got likelier the more Markdown previews you had open.

The setup: SVG badges in the Markdown preview

Editora renders Markdown to native JavaFX nodes (no WebView), and that includes images. README files are full of shields.io badges, which are served as image/svg+xml. JavaFX’s image decoder can’t read SVG, so I rasterize them with JSVG, a small pure-Java SVG library, into a BufferedImage and copy that into a JavaFX image.

That BufferedImage is the catch. Rasterizing through Java2D pulls in java.awt, and on macOS the AWT/Java2D native pipeline (AppKit/Metal) and JavaFX’s own Glass/Prism pipeline both want the single AppKit run loop. Put them in the same process, ask them to render at the same time, and occasionally they deadlock. The more badges being rasterized (the more previews open), the likelier the hang.

The fix: rasterize in software

java.awt.headless=true tells Java2D to use its software rasterizer instead of the native AppKit pipeline. The badge still renders to a BufferedImage exactly as before: it just never touches AppKit, so the contention disappears. JavaFX keeps the screen to itself.

Two details matter:

A free security win

There’s a nice side effect. JSVG loads with LoaderContext.createDefault(), which denies external resource loading, so an SVG can’t be coaxed into fetching a remote URL while it rasterizes. A malicious badge in some README can’t turn the preview into an SSRF vector. The headless software path keeps the whole thing self-contained.

The lesson

Mixing two UI toolkits in one process is asking for trouble, and “I’m only using AWT to make a BufferedImage, not a window” isn’t the escape hatch it looks like: on macOS, initializing Java2D is enough to wake AppKit. If you ever need Java2D purely for offscreen rendering inside a JavaFX (or SWT, or any non-AWT) app, set java.awt.headless=true up front and save yourself the intermittent-hang bug report that’s almost impossible to reproduce on demand.