Skip to main content
All case studies
Warning Dependency mismatch July 31, 2026

The crash that printed nothing

A feed reader crash-looped with exit 1 and zero output. The trail led through empty journals, silent JVMs, and finally bytecode, to an app compiled for a newer Java than the runtime.

Time to root cause
40 min
13:29 to 13:51, zero false fixes
Downtime
≈ 2 h
service down until the Java switch
Commands
183 / 24 h
10 messages in session
Data lost
None
H2 database left intact

The symptom

The CommaFeed service on container 319 was down. Systemd had restarted it five times and given up. Every start ended the same way: the Java process died in about one second with exit code 1, and printed nothing. Not to the journal, not to a log file, not anywhere.

Resources were fine. Memory, disk, and CPU all held normal values, and the container itself stayed up. The crash was inside the application, and it was refusing to explain itself.

The investigation

The agent worked an elimination path. Run the jar manually: silent. Swap the log manager: silent. Capture through a pseudo-terminal: silent. Ask the JVM itself with -Xlog:exceptions: nothing thrown at all. Each dead end narrowed the shape of the fault: the process was exiting deliberately, before any logging layer could speak.

Two clues turned the case. The package log showed an apt upgrade the day before had updated the installed Java 17 and added a Java 25 toolchain. And class-loading traces showed the JVM dying a fraction of a second after loading the bootstrap classes, with no exception ever raised.

To find who was swallowing the error, the agent read the application's own bytecode with javap. The bootstrap entry point routes errors through a delayed log handler and exits. System.exit runs before the handler flushes, so the stack trace is buffered and lost. The error existed the whole time; the app was holding it.

Root cause

UnsupportedClassVersionError. The updated application was compiled for class file version 69, which is Java 25. The container's default runtime was Java 17, which understands up to version 61. The first class the app loaded was too new for the JVM, and the version check failed before logging initialized.

The upgrade that broke it never touched the app's config. The old release ran on Java 17; the new release silently raised its compile target, and nothing in the update path warned that the runtime needed to move with it.

The fix

Java 25 was already installed on the box from the day-before upgrade, so the fix was one switch: update-alternatives pointing the default java at the Java 25 toolchain, then restart the service.

Before proposing it, the agent verified the path was sound by running the application directly under the new JVM. It booted clean, banner and all. The switch was then applied, and the service came back.

The result

The service returned after roughly two hours of downtime, with the database untouched and no data lost. Root cause was isolated in about forty minutes, without a single guess applied to the system.

The whole diagnosis was then written into the homelab wiki as a playbook, including a class-file-version reference table so the next person can skip most of the investigation.

Incident timeline

  • Service down

    13:29 · five crashes, systemd gives up restarting

  • Silent exit

    13:34 · exit 1 in ~1 s, zero output, resources normal

  • No trace anywhere

    13:37 · journal, log files, JVM crash logs: all empty

  • Upgrade found in package log

    13:34 · day before: Java 17 updated, Java 25 added

  • Class loading traced

    13:44 · JVM dies ~0.8 s after bootstrap; no exception raised

  • Bytecode read

    13:47 · javap shows the delayed handler that eats the trace

  • Root cause named

    13:51 · class file 69 needs Java 25; runtime is 17

  • Fix verified

    13:51 · app boots clean under the installed Java 25

  • Documented

    14:09 · playbook written into the wiki with a version table

Command log

Signature commands

pct exec 319 -- systemctl status commafeed
exit 3 Exit 3: crashed five times, systemd gave up
pct exec 319 -- bash -c "cd /opt/commafeed && java -verbose:class -jar quarkus-run.jar 2>&1 | tail -40"
exit 0 Class loading stops at bootstrap. The JVM dies before logging exists
pct exec 319 -- javap -c -p -cp /opt/commafeed/lib/boot/io.quarkus.quarkus-bootstrap-runner-3.37.2.jar io.quarkus.bootstrap.runner.QuarkusEntryPoint
exit 0 Read the bytecode to find who swallows the stack trace
pct exec 319 -- bash -c "/usr/lib/jvm/temurin-25-jdk-amd64/bin/java -jar /opt/commafeed/quarkus-run.jar | head -30"
exit 0 Proof of fix: the app boots clean under Java 25
Command log
Time Agent Command Exit
13:39:48 SCAIRE pct exec 319 -- systemctl status commafeed failed state: five start attempts, then systemd quit exit 3
13:39:49 SCAIRE pct exec 319 -- journalctl -u commafeed --no-pager -n 80 journal holds lifecycle only; the error never lands exit 0
13:34:49 SCAIRE pct exec 319 -- grep -i "upgrade\|install" /var/log/dpkg.log | tail -30 finds the Jul 30 Java change that set the trap exit 0
13:35:37 SCAIRE pct exec 319 -- systemd-run --pipe --wait --working-directory=/opt/commafeed java -jar quarkus-run.jar piped stdout and stderr: still nothing exit 1
13:36:30 SCAIRE pct exec 319 -- java -Dorg.jboss.logging.provider=jul -jar /opt/commafeed/quarkus-run.jar | head -30 alternate logging stack: silent as well exit 0
13:43:57 SCAIRE pct exec 319 -- bash -c "cd /opt/commafeed && java -verbose:class -jar quarkus-run.jar 2>&1 | tail -40" class loading stops at bootstrap; nothing after exit 0
13:45:39 SCAIRE pct exec 319 -- unzip -l /opt/commafeed/quarkus-run.jar thin jar: manifest only, normal for Quarkus exit 0
13:46:55 SCAIRE pct exec 319 -- javap -c -p -cp /opt/commafeed/lib/boot/io.quarkus.quarkus-bootstrap-runner-3.37.2.jar io.quarkus.bootstrap.runner.QuarkusEntryPoint reads the entry point bytecode for the error path exit 0
13:47:27 SCAIRE pct exec 319 -- javap -c -p -cp /opt/commafeed/lib/boot/io.quarkus.quarkus-bootstrap-runner-3.37.2.jar io.quarkus.bootstrap.runner.QuarkusEntryPoint | grep -A 200 "private static void doRun" the doRun body: rethrow is real, the flush is the leak exit 0
13:48:14 SCAIRE pct exec 319 -- script -qec "java -jar /opt/commafeed/quarkus-run.jar" /dev/null pseudo-terminal capture: still zero output exit 1
13:49:26 SCAIRE pct exec 319 -- bash -c "java -Xlog:exceptions=info -jar /opt/commafeed/quarkus-run.jar | head -20" JVM-level exception log: nothing thrown exit 0
13:49:43 SCAIRE pct exec 319 -- update-alternatives --list java Java 25 is already installed from the Jul 30 upgrade exit 0
13:49:58 SCAIRE pct exec 319 -- /usr/lib/jvm/temurin-25-jdk-amd64/bin/java -version Java 25.0.4 LTS verified working exit 0
13:51:04 SCAIRE pct exec 319 -- bash -c "/usr/lib/jvm/temurin-25-jdk-amd64/bin/java -jar /opt/commafeed/quarkus-run.jar | head -30" the app boots clean under Java 25: root cause proven exit 0

From the session

Operator
Something is wrong with the commafeed service on the pct 319 container. Please investigate.
SCAIRE
Java's crashing within one second of startup, and systemd isn't capturing the actual error. Memory and disk are fine, so this isn't starvation. The JVM exits with code 1 and prints nothing. That's unusual.
SCAIRE
Found the smoking gun. UnsupportedClassVersionError: the app was compiled by a more recent Java runtime, class file version 69, and this runtime only recognizes up to 61. Quarkus's delayed log handler swallowed the stack trace because the exit ran before it could flush.
Operator
That worked, but document it on the homelab part of the wiki.
SCAIRE
Done. Created as a playbook under the troubleshooting collection: symptoms, root cause, why there was no output, the diagnosis flowchart we walked, the fix, and a class-file-version table so the next person skips most of this.

Lessons

A silent crash still leaves evidence: class-load traces, package logs, and bytecode. Escalate the tooling when the output is empty.
Quarkus buffers early log output behind a delayed handler, and System.exit bypasses the flush. Missing output is not proof there was no error.
Track application compile targets against the runtime. Class file 69 means Java 25; 61 means Java 17. The update that raised the target never warned.