Skip to content

Feedback Quarkus

Sanne's AI assistant edited this page Oct 26, 2026 · 9 revisions

Feedback: Quarkus

Findings from the Quarkus 3.40.1.1 to 4.0.0.Beta1 upgrade, recorded in #1714 and PR #1693 (a redo of #1670). The full record is docs/dev/QUARKUS-4.md on the quarkus-4-redo branch. Vert.x 5 findings, with their single-file reproducers, are on Feedback: Vert.x; this page keeps what belongs to Quarkus. Letters (B, E, ...) and codes (Q-*, V-*) refer to that record and to #1714. Nothing here has been filed upstream yet.

Each Quarkus item has two follow-ups for Sanne:

  • Verify: how to check it on Quarkus main, with the smallest reproducer we have.
  • Discuss with the team: the upstream change we propose, and what it costs.

Every item below was re-checked on Quarkus main at 23e9ff0c4d3 (2026-10-26: Vert.x 5.2.2, Netty 4.2.19.Final, Jackson 3.1.8 and 2.21.8). Sizes come from a minimal native app with only quarkus-vertx, built with GraalVM 25.0.4 and -H:+BuildReport.

Known upstream issues we wait on

None so far: neither #1714 nor #1693 links an upstream Quarkus, Vert.x or Netty issue.

Findings

  • B. Netty's async DNS resolver is always linked into native quarkus-vertx apps (Q-1, low priority). It costs about 0.27 MB in a minimal app, which most apps never use; the behaviour at run time is correct. Worked around in isx with VertxDnsSubstitutions, which is fine there because isx never enables the async resolver. (#1714)

    • Verify: the -J-Dvertx.disableDnsResolver=true that VertxCoreProcessor passes to native-image only reaches the builder JVM. At run time VertxCoreRecorder.convertToVertxOptions sets vertx.disableDnsResolver=true unless quarkus.vertx.use-async-dns=true, and Vert.x 5 reads it in io.vertx.core.spi.dns.AddressResolverProvider.factory. A native app that prints NameResolver's provider gets DefaultAddressResolverProvider by default, and DnsAddressResolverProvider with -Dquarkus.vertx.use-async-dns=true: the async resolver works in native today. A substitution of factory that returns new DefaultAddressResolverProvider() removes 117 types and about 0.27 MB (0.19 MB of file size; isx's larger proxy measured 1.2 MB), but then use-async-dns=true is silently ignored.
    • Discuss with the team: the size can only be saved with a build-time decision, because use-async-dns is a run-time property. That means a native build-time property, or a substitution guarded by onlyWith on build-time config, and a documented limit that the async resolver then cannot be enabled at run time. Probably not worth a new knob for 0.27 MB; worth raising only alongside other native size work (Q-4).
  • E. Quarkus 4 silently switches every app to Netty's adaptive allocator, which makes allocator-max-order inert; and io.netty.noUnsafe=true in native is undocumented for users (Q-7, Q-8). (#1714) Netty chooses its default buffer allocator from the system property io.netty.allocator.type (pooled, unpooled or adaptive, read in ByteBufUtil). When the property is not set, Netty's own default applies, and that default changed: pooled in Netty 4.1 (Quarkus 3.x), adaptive in Netty 4.2 (Quarkus main). Quarkus sets several Netty system properties itself (io.netty.allocator.maxOrder=3, io.netty.leakDetection.level, and io.netty.noUnsafe in native), but not io.netty.allocator.type. So moving to Netty 4.2 moved every app from the pooled to the adaptive allocator, only because Netty's default changed. This is also why quarkus.netty.allocator-max-order stopped working: it sets io.netty.allocator.maxOrder, which only the pooled allocator reads. On 4.1 that was the default, so the setting worked; on 4.2 the adaptive allocator ignores it, with no warning. A user can set -Dio.netty.allocator.type=pooled themselves, but few would know they need to.

    • Verify: in Netty 4.2.19, ByteBufUtil defaults io.netty.allocator.type to adaptive, and only PooledByteBufAllocator reads io.netty.allocator.maxOrder. Vert.x 5.2's VertxByteBufAllocator takes ByteBufAllocator.DEFAULT, and nothing in Quarkus sets the type or uses the pooled allocator. In JVM mode, four 32 MB TLS relays left 68 MB of direct memory with the adaptive allocator against 8 MB pooled; it is off-heap, so heap limits do not cap it. The native measurement is still open on our side (D-3). NettyProcessor.build25Specific sets io.netty.noUnsafe=true as a native-image system property; its javadoc explains why (JEP 471, quarkus#39907), but no guide does.
    • Discuss with the team: set io.netty.allocator.type explicitly, so the choice is Quarkus's and documented rather than inherited from Netty. Either back to pooled, keeping the 3.x behaviour and making allocator-max-order work again, or to adaptive on purpose, with a migration-guide note and a warning when allocator-max-order is set. Either way, the allocator-max-order javadoc should say it only applies to the pooled allocator. pooled trades the adaptive allocator's throughput for predictable memory; the choice needs their own benchmarks. Also document noUnsafe and why it is set.
  • F. Vert.x 5 API removals and renames need hand ports. The Quarkus part is recipes (I); the list is on Feedback: Vert.x. (#1714)

  • G. Vert.x 5 behaviour changes that still compile. The Quarkus part is a migration-guide list (J); the findings are on Feedback: Vert.x. (#1714)

  • H. Jackson BOM import order (Q-5). The Jackson 3 BOM is still imported before the Jackson 2 one in bom/application/pom.xml, so jackson-annotations comes from Jackson 3's BOM. It is consistent today only because c651056c3c6 set Jackson 2 back to 2.21.8, which matches Jackson 3.1.8's annotations 2.21. The next Jackson 2 bump past 2.21 (Dependabot already did 2.22.2 to 2.22.3) brings back the mismatch we hit: databind 2.22 with annotations 2.21. (#1714)

    • Verify: with com.fasterxml.jackson.core:jackson-databind and the 999-SNAPSHOT BOM, mvn dependency:tree -Dincludes=com.fasterxml.jackson.core shows databind 2.21.8 and annotations 2.21.
    • Discuss with the team: manage jackson-annotations explicitly at the version both Jackson lines accept, or swap the imports, or make the two versions move together.
  • I. quarkus update has no Vert.x 5 recipes, and the Jackson recipe breaks a catch (Q-6). (#1714)

    • Verify: quarkus-update-recipes 1.15.4 (2026-10-15) has 4.0 recipes for Java 21, Jackson 3, config renames, ArC and dropped relocations. It has none for the Vert.x 5 renames (F), the CDI API groupId (jakarta.enterprise:jakarta.enterprise.cdi-api to jakarta.cdi:jakarta.cdi-api) or the smallrye-common-vertx-context rename. The Jackson part delegates to OpenRewrite's UpgradeJackson_2_3; with rewrite-jackson 1.29.0 it turns catch (IOException e) around createGenerator into catch (JacksonException | IOException e) and leaves the one around gen.close(). Both then fail with exception java.io.IOException is never thrown in body of corresponding try statement. It also does not keep Jackson 2 defaults.
    • Discuss with the team: 4.0 recipes for the Vert.x 5 renames (F), the CDI API groupId and the smallrye-common-vertx-context rename, plus TODO markers for the behaviour changes (G). Recipes cannot make behaviour changes safe, so markers are the honest limit there. The catch bug belongs to openrewrite/rewrite-jackson.
  • J. Migration guide gaps (Q-9, Q-10). (#1714)

    • Verify: the 4.0 migration guide (Migration-Guide-4.0 on the Quarkus wiki) has a Jackson section with a defaults table, but no Vert.x 4 to 5 or Netty 4.2 section, nothing on Jackson 2 still being managed next to a Jackson 3 quarkus-jackson, no SORT_PROPERTIES_ALPHABETICALLY in the defaults table, and neither the CDI API groupId nor the smallrye-common-vertx-context rename. Q-10: every native build still prints Warning: Using a deprecated option --enable-url-protocols= from command line. Use reachability metadata instead. (NativeImageBuildStep), and the NativeConfig javadoc still names jdk-21 builder images.
    • Discuss with the team: add those sections, with a pointer to the Vert.x 5 migration guide, and the behaviour-change list from G. Dropping --enable-url-protocols may need reachability metadata in its place, so it is worth asking what replaces it.
  • Q-4. Native size growth (data, not a bug). The minimal app grows by 2.04 MB from Quarkus 3.40.1 to main: 1.05 MB of machine code, 0.85 MB of image heap, 604 more types. The largest single part is Jackson 3 core, linked even with no JSON in use: 0.44 MB of machine code, against 0.07 MB for Jackson 2 core on 3.40.1. Then about 0.2 MB of FFM support for Netty 4.2's buffer cleaner, 0.23 MB for the Quarkus runtime and SmallRye Config, and 0.06 MB of Netty resolver (B). Vert.x core itself adds only about 0.04 MB, and io.netty.buffer shrinks by 0.15 MB.

    • Verify: compare -H:+BuildReport machine-code trees for a minimal quarkus-vertx app on 3.40.1 and main.
    • Discuss with the team: what makes Jackson 3 core reachable in an app that never touches JSON, and whether native size per extension is tracked between releases. Dropping the unconditional JacksonCodec reflection registration in VertxCoreProcessor does not help: it saves 3 types and 272 bytes.
  • Q-11. Reflection-free Jackson outside REST (suggestion). The build-time generated (de)serializers still exist only in quarkus-rest-jackson (quarkus.rest.jackson.optimization.enable-reflection-free-serializers), for REST endpoint types.

    • Verify: nothing to reproduce.
    • Discuss with the team: whether the generator could be offered for an application's own types and ObjectMappers. The cost is a public API to maintain.
  • Vert.x-only items (V-T*, V-W*, V-H*, V-P1) are on Feedback: Vert.x.

Clone this wiki locally