App Suite Releases
  • 8.47
  • 8.35
  • 7.10.6
Imprint
  • 8.47
  • 8.35
  • 7.10.6
Imprint
  • Release 8.51
    • Noteworthy Changes
      • Important Changes
      • App Suite Middleware
    • Changelogs
      • App Suite UI
      • App Suite Middleware
      • Additional Components
        • AI Service
        • OX Guard UI
        • Switchboard
        • Changelog
    • Helm Charts
      • AI-Service documentation
      • App Suite Stack Chart
      • Helm Chart core-cacheservice
      • Helm Chart core-documentconverter
      • Helm Chart core-imageconverter
      • core-mw
      • UI Service
      • Switchboard
  • Release 8.50
  • Release 8.49
  • Release 8.48
  • Release 8.47
  • Release 8.46

App Suite Middleware

8.51.99

General

SCR-1793

Summary: Redis connector: per-node client name, max. connection lifetime, deterministic shutdown

Hardening of the Redis connector against stale / orphaned connected clients.

New configuration option:

  • com.openexchange.redis.connection.pool.maxLifetimeSeconds Maximum lifetime in seconds of a pooled Redis connection. Once a connection exceeds this age it is proactively recycled by the connection-pool cleaner as soon as it becomes idle, regardless of usage; this applies to both the shared and the dedicated pool. Acts as defense-in-depth against slowly accumulating or long-lived stale connections that TCP keepalive cannot reap (a live-but-idle connection is never detected as dead). A value of 0 (zero) disables max. lifetime recycling. Default 3600 (one hour). Not reloadable, not config.cascade aware. Package: open-xchange-core.

Behavioral changes (no configuration):

  • Client name: the announced Redis client name now includes the local host / pod name (e.g. Open-Xchange-Redis-Connector-v8.53.0-<host>), so connections become attributable per node via Redis CLIENT LIST. This is what lets operators tell restart orphans (dead pod addresses) apart from live-node connections.
  • Deterministic shutdown: the shared connection pool now closes its connections synchronously on shutdown, so Redis reclaims the clients immediately on a graceful (rolling) restart instead of leaving them as ghosts.

8.51.95

Behavioral Changes

SCR-1723

Summary: Java 25: virtual-thread HTTP worker pool and opt-in generational ZGC

Full admin guide: https://documentation.open-xchange.com/8/middleware/administration/garbage_collection_and_memory_sizing.html

Change

The core middleware now runs on Java 25 (JDK 25 runtime). Operationally relevant defaults that change with this upgrade:

  • Virtual-thread HTTP worker pool — the Grizzly request workers run on virtual threads by default (com.openexchange.http.grizzly.virtualThreadsEnabled=true), raising in-flight request concurrency.
  • Compact Object Headers (-XX:+UseCompactObjectHeaders, JEP 519) are enabled via javaOpts.other, reducing live heap.

The garbage collector default is unchanged: G1 stays the default. Generational ZGC is available as an opt-in alternative via the Helm value javaOpts.zgc (default false); set to true it appends -XX:+UseZGC to the JVM options. (An earlier revision shipped ZGC as the default; it was reverted to opt-in before release — see "Why opt-in, not default" below.) ZGC is the only change with deployment-sizing impact, and only when enabled (see below). The worker-pool defaults that accompany the virtual-thread switch are listed at the end.

What an administrator must do

Nothing is required. G1 stays the default garbage collector, so existing memory sizing is unaffected by the JDK 25 upgrade as far as the collector is concerned.

Who should opt in to ZGC. With the MALLOC_ARENA_MAX=2 default, ZGC's former native-memory penalty is largely gone - its non-heap native sits at G1 level and the only remaining premium is the eager heap commit, which converges with G1 under a realistic live set. The load test (below) showed ZGC faster than G1 - better mean and tail latency and higher throughput - even at a modest 4G/6G pod, so ZGC is a sound choice for virtual-thread, latency-sensitive deployments generally, not just large installations. G1 remains the default and the better fit for throughput-/batch-bound workloads, very small heaps (where G1 is more CPU-efficient), and CPU-starved pods (ZGC's concurrent GC needs CPU headroom). Validate under your own load before tightening below 4G/6G - the benchmark had a small heap live set and under-exercised mail/attachment-heavy paths.

If you do opt in (javaOpts.zgc: true), size for ZGC's native-memory headroom. With MALLOC_ARENA_MAX=2 set (now a chart default - see "Why the headroom is large" below), the load-validated configuration is a 4G heap on a 6G container limit:

  • javaOpts.memory.maxHeapSize: 4G (or javaOpts.memory.maxRAMPercentage: "50")
  • resources.requests.memory: 6G and resources.limits.memory: 6G
  • MALLOC_ARENA_MAX=2 on the container env (chart default) - without it the same workload needs an 8G limit.
  • give the pod adequate CPU - concurrent GC needs headroom; do not run ZGC CPU-starved. This was hard-validated under load (50 concurrent users, real Dovecot/Postfix): cgroup peak ~5.3G, 0 OOM, 0 restarts, no request errors. Without MALLOC_ARENA_MAX the peak is ~6.5G and a 6G limit fails (matching the earlier CI observation that 4G/6G failed). Too little memory does not surface as an OOM - it shows up as failures to open outbound IMAP/SMTP connections (mail-backend timeouts) under load, a downstream effect of the container exhausting native memory, not the mail sockets themselves. The test had a small heap live set and under-exercised mail/attachment-heavy paths, so treat the peak as a floor and keep margin - do not tighten below 6G without a heavier-mail re-test.

Why the headroom is large, and how to shrink it (load-tested 2026-06-29). Native-memory tracking under load overturns the original assumption that direct buffers dominate: direct/off-heap memory (NMT "Other") peaks at only ~0.3-0.5G and ZGC's own structures at ~80MB. The real driver of the excess is glibc malloc-arena retention (~1.2G) - the many-threaded virtual-thread middleware spawns many per-thread malloc arenas that hoard memory. Setting MALLOC_ARENA_MAX=2 (container env, now a chart default) caps this and cuts the cgroup peak from ~6.5G to ~5.3G at a 4G heap, letting ZGC fit a 6G limit. Crucially this brings ZGC's non-heap native memory down to the same level as G1 (~1.25G in both with the cap) - the only remaining difference is that ZGC eager-commits its heap while G1 right-sizes, and that gap converges under a realistic (larger) heap working set. -XX:MaxDirectMemorySize is not a useful sizing lever here (direct memory is small); it is only worth setting as an optional fast-fail cap.

(If core-mw is deployed as a subchart, the keys live under the core-mw: block.)

Returning idle memory (pay-per-used hosting). On hosting billed by actual memory use (RSS), ZGC hands idle heap back to the OS. Set javaOpts.zgcUncommitDelay (e.g. "60") to uncommit unused heap sooner than the 300s default (-XX:+ZUncommit is on by default). A 20-min soak confirmed ~2.2G returned to the OS within ~4 min of load dropping, while staying KO=0 under load. This returns heap only - glibc malloc arenas are bounded separately by MALLOC_ARENA_MAX. Trade-off: re-commit costs page faults when load returns; too short a delay churns under spiky load. If billed on the pod reservation rather than RSS, right-size the request instead. Do not throttle the heap with -XX:SoftMaxHeapSize to force a smaller footprint - a soak with SoftMaxHeapSize=2G at a 4G heap caused a severe tail regression (request timeouts, max 37.5s) by starving ZGC of allocation headroom; the idle give-back does not need it. (For very spiky, small-live-set workloads G1 reclaims even more aggressively, at a worse tail.)

Load-test results - performance and sizing

A full-stack load test (self-deployed core-mw-test umbrella chart with real Dovecot/Postfix/DB/Redis, in-cluster Gatling AppSuiteSimulation, 50 concurrent users; JDK 25, generational ZGC, 4G heap) measured ZGC against G1 at an identical 4G heap / 6G limit with MALLOC_ARENA_MAX=2, both error-free (KO=0):

  • ZGC is faster, not slower: mean 38 vs 56 ms, p95 106 vs 130 ms, p99 213 vs 796 ms, max 1466 vs 6118 ms, and +47% throughput (1.03M vs 0.70M requests in the same window). ZGC's occasional allocation stalls cost far less than G1's multi-second stop-the-world pauses on this allocation-heavy, virtual-thread workload.
  • Memory: ZGC cgroup peak ~5.3G vs G1 ~1.3G. The gap is not GC overhead - with MALLOC_ARENA_MAX=2 the non-heap native is ~1.25G for both. It is ZGC eager-committing the 4G heap (file-backed) while G1 right-sized to ~370M for this small test live set; G1 could not be forced to hold 4G (-Xms4G/AlwaysPreTouch spiked then uncommitted). Under a production-sized live set the two converge.
  • Conclusion: with MALLOC_ARENA_MAX=2 the memory premium of ZGC shrinks to its eager heap commit (small in practice) while the latency/throughput win is clear. ZGC is therefore recommended for the virtual-thread worker pool (latency-sensitive deployments); G1 remains the safe default for tight or throughput-bound pods.
Why opt-in, not default

ZGC was initially made the default but reverted to opt-in before release. Defaulting it on would force a mandatory pod re-sizing onto every installation at upgrade time; an operator that did not re-size would silently hit the native-memory failure mode above (IMAP/SMTP connection failures, not an obvious OOM) — a poor default for large/cloud deployments. G1 has zero such sizing impact. The opt-in default is retained for conservatism (no forced re-sizing at upgrade time), but the latency/throughput benchmark has since been run (see "Load-test results" above) and favours ZGC: with MALLOC_ARENA_MAX=2 resolving most of the memory premium, ZGC is now the recommended collector for virtual-thread, latency-sensitive deployments, and a future release may reconsider it as the default.

Why ZGC, and why it fits the virtual-thread worker pool

The middleware request path now runs on a virtual-thread worker pool, which raises in-flight concurrency and the rate of short-lived, request-scoped allocations. G1's stop-the-world young/mixed collections scale with heap/live-set and pause all carrier threads at once, causing latency spikes across many virtual threads. Generational ZGC collects concurrently with sub-millisecond, heap-size-independent pauses, and its young generation suits exactly this short-lived-allocation pattern, giving stable tail latency under high concurrency. Compact Object Headers (also default) reduces live heap and further eases GC pressure. This synergy is why ZGC is offered as an opt-in for latency-sensitive, well-sized deployments.

Concerns / when to stay on G1
  • Native-memory headroom (see above) - largely mitigated by the MALLOC_ARENA_MAX=2 chart default, which brings ZGC's non-heap native down to G1 level and lets it fit a 6G limit; still budget the headroom and validate under load for mail/attachment-heavy workloads.

  • CPU: ZGC trades some throughput/CPU for low pauses. On CPU-starved pods its concurrent GC threads compete with the virtual-thread carriers and can raise latency. Ensure CPU headroom.

  • Allocation stalls: if the allocation rate outpaces concurrent collection (heap or CPU too small), ZGC stalls threads until memory is freed — the failure mode to watch under load spikes (monitor for "Allocation Stall" GC log lines).

  • Stay on G1 (the default) for very tight containers, throughput-/batch-bound workloads, or very small heaps where G1 is more CPU-efficient.

  • Virtual-thread pinning is orthogonal — ZGC does not pin virtual threads.

  • The latency/throughput benchmark has now been run (see "Load-test results" above) and favours ZGC; it used a small heap live set and under-exercised mail/attachment-heavy paths, so a heavier-mail load test is still advisable before flipping ZGC to the default in a future release.

About the virtual-thread worker pool

Virtual threads (JEP 444, stable since JDK 21) are lightweight JVM-managed threads multiplexed onto a small pool of OS "carrier" threads. A virtual thread blocked on I/O (IMAP/SMTP/DB) unmounts its carrier instead of holding an OS thread, so the server keeps far more requests in flight at roughly a stack's cost each rather than a full platform thread.

  • Benefit: HTTP throughput under high concurrency is no longer capped by a bounded worker pool, while blocking I/O stays simple (no async rewrite).
  • Behavioral changes: concurrency is no longer throttled by pool exhaustion — global back-pressure is now com.openexchange.threadpool.virtual.maxConcurrency (default auto); more in-flight requests mean higher peak heap/native use (hence the ZGC sizing above applies when ZGC is enabled); thread dumps show many short-lived virtual threads instead of a fixed named pool, and pool-saturation metrics no longer apply to HTTP work.
  • Fallback: com.openexchange.http.grizzly.virtualThreadsEnabled=false reverts to the platform-thread worker pool.
  • Virtual-thread pinning (a VT stuck to its carrier during synchronized/native sections) was audited — middleware code shows none; ZGC does not pin either.
Worker thread pool defaults (changed)

The shared worker thread pool ("OXWorker") previously shipped with an unbounded maximumPoolSize and a synchronous hand-off queue, allowing unbounded platform-thread creation under load. The Grizzly HTTP worker pool now runs on virtual threads by default (com.openexchange.http.grizzly.virtualThreadsEnabled=true) and is not governed by this pool; the bounded default guards non-HTTP work and the virtualThreadsEnabled=false fallback.

  • com.openexchange.threadpool.maximumPoolSize Maximum number of platform threads in the shared worker pool. Shipped default changed 2147483647 -> 2000. Not reloadable, not config-cascade aware. File: threadpool.properties.

  • com.openexchange.threadpool.workQueue Queue type for the shared worker pool. Shipped default changed synchronous -> linked. Combined with maximumPoolSize greater than corePoolSize this activates the ScalingQueue: threads scale up to maximumPoolSize, then excess tasks queue instead of spawning further threads. Not reloadable, not config-cascade aware. File: threadpool.properties.

  • com.openexchange.threadpool.virtual.maxConcurrency The maximum number of tasks that may run concurrently on the shared virtual-thread executor. Acts as a global back-pressure limit: once reached, submission of further tasks blocks until a running task completes. This is a process-wide overload limit, not a per-caller setting; individual fan-out sites may apply their own, narrower concurrency bound on top of it. Accepts a positive integer or the special value auto (default). With auto the limit is derived best-effort at start-up from the maximum heap size (the dominant constraint on in-flight request memory) and the active garbage collector: it scales with the heap and is reduced slightly under ZGC, which needs more native-memory headroom. The derived value is clamped to [512, 20000] and logged with its inputs at start-up (roughly ~4000 at a 4G heap on G1, ~3300 on ZGC). An explicit positive integer overrides the automatic value; a non-positive or unparseable value falls back to auto. Default auto (previously the fixed value 20000). Not reloadable, not config-cascade aware. File: threadpool.properties. Note: this is a memory-OOM safeguard, not a concurrency tuning target — the useful concurrency of blocking virtual-thread fan-out is almost always bound by a downstream pool (database connections, mail access, etc.) below this ceiling, which enforces its own narrower limit. The auto estimate is tunable via com.openexchange.threadpool.virtual.maxConcurrency.auto.heapFraction and com.openexchange.threadpool.virtual.maxConcurrency.auto.perRequestKB (below).

  • com.openexchange.threadpool.virtual.maxConcurrency.auto.heapFraction Fraction of the maximum heap budgeted for transient per-request state by the auto derivation of com.openexchange.threadpool.virtual.maxConcurrency; only consulted when that property is auto. Must be a decimal in (0, 1]; an absent, out-of-range or unparseable value falls back to 0.25. Not reloadable, not config-cascade aware. File: threadpool.properties.

  • com.openexchange.threadpool.virtual.maxConcurrency.auto.perRequestKB Estimated transient heap (in KiB) per in-flight request used by the auto derivation of com.openexchange.threadpool.virtual.maxConcurrency; only consulted when that property is auto. Raise it for requests with large transient state (buffered attachments, large responses) to make the safeguard more conservative; lower it for lightweight workloads. Must be a positive integer; an absent, non-positive or unparseable value falls back to 256. Not reloadable, not config-cascade aware. File: threadpool.properties.

8.51.89

3rd Party Libraries/License Change

SCR-1724

Summary: Upgraded OSGi core library

Upgraded OSGi core library in target platform (com.openexchange.bundles):

  • eclipse.osgi_3.24.0.v20251126-0427.jar upgraded to org.eclipse.osgi_3.24.200.v20260515-1403.jar

API - HTTP-API

SCR-1726

Summary: "sanitize_css" parameter for /mail?action=get

Summary

Adds an optional query parameter sanitize_css to the /mail?action=get endpoint. It lets a client decide per request whether CSS content in HTML mail is sanitized against the white-list (in CleaningJsoupHandler and CssOnlyCleaningJsoupHandler), instead of always sanitizing.

Parameter

||Name||Location||Type||Required||Description|| |sanitize_css|query|boolean|no|Controls whether CSS content in HTML mails is sanitized against the white-list.|

Values

||Value||Behavior|| |(omitted)|CSS is sanitized – unchanged behavior.| |true|CSS content is sanitized against the white-list.| |false|CSS content is passed through unfiltered.|

Notes
  • Default is to sanitize: when the parameter is absent CSS sanitizing is performed exactly as before, so existing requests are unaffected.
  • Affects CSS sanitizing only; HTML tag white-listing (the sanitize parameter) and external-image handling (the replace_external_images parameter) are independent.
  • No configuration change and no API-breaking change.

API - REST

SCR-1714

Summary: New Administrative REST Servlet for Shared Accounts

Permissions and capabilities a particular user effectively has for a shared account are not persisted as such, but evaluated dynamically at runtime - based on the base configuration and all shared account permissions the user received, directly as well as indirectly through his group memberships. Since this calculated result can therefore not be deduced directly from the provisioned data, a dedicated administrative REST interface is available that answers the question: which effective permissions and capabilities does a certain user have for a shared account?

The endpoints are exposed below /preliminary/sharedaccounts/v1 and are protected via HTTP Basic Authentication, with the credentials configured through the properties com.openexchange.rest.services.basic-auth.login and com.openexchange.rest.services.basic-auth.password.

The user whose access is to be evaluated - and, where applicable, the targeted shared account - can be referenced in three alternative ways: by their explicit internal identifiers, by their email address, or by their mail login string.

See the general documentation, as well as the REST API documentation for further details.

Behavioral Changes

SCR-1717

Summary: Deny Write-Access for Guest Users in Public Calendar Folders

With MW-1473 write access for invited guest users was introduced. However, write access in a calendar folder also implies taking over the organizer role for scheduled appointments. This role cannot be hijacked for a foreign principal residing on an external calendaring and mail system a guest user originates from. Therefore, guest users may only receive write access for personal or shared calendar folders that are bound to a known internal calendar user they can act on behalf of, but not for public calendar folders.

See the documentation for further details.

Configuration

SCR-1583

Summary: Per-user Filters for LDAP Contacts Provider

Besides the distinguishing attribute placeholder [value] for the folders, the filter template in mode dynamicAttributes may now also contain several user- or session-specific placeholders which are replaced dynamically from the requesting user's session prior passing the query to LDAP. Doing so, it is possible to model different 'views' on the data, in case the user base is also represented through the LDAP directory, in combination with multi-value LDAP attributes.

See the documentation for the new replacement options and further details.

Database

SCR-1718

Summary: Update Task to Downscope Unsupported Guest Permissions

The blocking database update task 'com.openexchange.groupware.update.tasks.DownscopeGuestPublicCalendarPermissionsTask' is introduced to downscope write permissions of guest users on public calendar folders to "read-only".

With MW-1473 write access for invited guest users was introduced. However, write access in a calendar folder also implies taking over the organizer role for scheduled appointments. This role cannot be hijacked for a foreign principal residing on an external calendaring and mail system a guest user originates from. Therefore, guest users may only receive write access for personal or shared calendar folders that are bound to a known internal calendar user they can act on behalf of, but not for public calendar folders.

This task aligns already existing permissions accordingly by stripping object-write, object-delete, sub-folder/create-object as well as administrative folder permissions from any guest user permission on a public calendar folder, leaving folder- and object-read permissions untouched.

Prev
Important Changes