Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Event Listener module
Job module
Language module
Macro Module
Servlet Filter module
Servlet module
Theme module
Web UI modules
Workflow module
Last updated Sep 22, 2026

Asynchronous macro execution

AvailabilityConfluence 11.0 or later

Experimental in Confluence 11.0

Asynchronous macro execution is off by default. Behavior, configuration, and APIs may change before this feature becomes supported. Validate this guide against each release until the feature is stable.

This page is aimed at app developers who maintain macro modules. It explains how asynchronous macro execution works, the contracts your macro accepts when it opts in, and how to implement concurrency safety, cancellation, and result caching.

Overview

Confluence normally executes every macro on a page sequentially on the Tomcat HTTP request thread. A slow macro can therefore delay the entire page.

With asynchronous macro execution, eligible macros are dispatched to a dedicated worker thread pool and can run concurrently. The feature is controlled by the atlassian.macros.async.execution dark feature, which is disabled by default.

How a conversion runs

The rendering pipeline has two phases:

  1. Walk: Confluence traverses the storage-format XML. Each eligible macro is submitted to the worker pool and a placeholder is recorded. The walk does not wait for the result.
  2. Write: Confluence writes placeholders in document order and waits for each macro's result as it reaches that placeholder.

Execution order and output order are different. The final HTML preserves document order, but eligible macros may execute concurrently in an arbitrary order. A macro must not depend on another macro having executed first.

Execution policies

Confluence applies these policies in priority order for each macro execution:

  1. Cache hit: Confluence returns a stored result without executing the macro.
  2. Deduplication: An identical in-flight execution attaches to the existing execution.
  3. New execution: Confluence submits a new task to the worker pool.

If the worker queue is saturated, the macro falls back to synchronous execution on the HTTP request thread.

Opt in to asynchronous execution

A macro is dispatched asynchronously only when all of the following conditions are met:

  1. The dark feature is enabled for the Confluence instance.
  2. The macro returns true from Macro.isConcurrentExecutionSafe().
  3. An administrator has enabled asynchronous execution for that macro.

The Macro interface exposes three opt-in methods. Each method defaults to false.

1
2
3
4
default boolean isConcurrentExecutionSafe() { return false; }  // since 11.0.0
default boolean isCancellable()             { return false; }  // since 11.0.0
default boolean isCacheable()               { return false; }  // since 11.0.0

No macro shipped with Confluence 11.0 overrides isConcurrentExecutionSafe(). Therefore, no product macro opts in by default.

An administrator can save an asynchronous setting for a macro that has not declared itself safe. Confluence stores the setting and logs a warning, but the setting has no effect until the macro declares support. The read-only supportedByMacro field in the REST response reflects the macro declaration.

Implement concurrent execution safety

Override isConcurrentExecutionSafe() to declare that your macro can run on a worker thread alongside other macros.

1
2
3
4
5
@Override
public boolean isConcurrentExecutionSafe() {
    return true;
}

This declaration is a contract. You are asserting that the macro produces correct output when it runs off the request thread and concurrently with other macros sharing the same ConversionContext.

The declaration covers everything the macro renders, not only its own execute() method. If the macro renders nested content, the declaration also asserts that the nested content is safe.

When it is safe to opt in

A macro is a reasonable candidate when all of the following are true:

  • Its output depends only on its parameters, body, and the supplied ConversionContext.
  • Every service it uses is resolved through the plugin container and is thread-safe.
  • It shares no mutable state between invocations, including static collections, caches, or counters written during execution.
  • Any nested content it renders contains only macros and transformers that you control.
  • It does not depend on another macro on the page having executed first.

When not to opt in

Do not declare a macro safe if it, or anything it renders:

  • Reads or writes an application-owned ThreadLocal, or relies on one set by a servlet filter.
  • Reads ThreadLocalCache or HttpServletRequest. Neither is available on a worker thread.
  • Writes to the request cache and expects the value to be available later. Worker-thread writes are made to a snapshot and discarded.
  • Read-modify-writes a ConversionContext property or mutates a non-thread-safe value stored in it.
  • Performs a compound operation on a shared object without its own synchronization.
  • Falls back to permissive behavior when expected state is missing, particularly during permission or visibility filtering.
  • Renders nested content containing macros or transformers that you do not control.

State available on the worker thread

Carried across dispatchNot carried across dispatch
Authenticated userThreadLocalCache
Request cache snapshot; writes are discardedHttpServletRequest
Logging context (MDC)Application-owned ThreadLocal
Tracing span
Scopes cache
Output type
Content include stack copy

Apps cannot add additional state to this set. Test with the dark feature enabled on a non-production instance before shipping the declaration.

Implement cancellation

How cancellation works

  1. A REST call publishes a Confluence event.
  2. The CancellationListener receives the event and calls the cancellation handler.
  3. The cancellation handler sends Thread.interrupt() to the execution thread and sets a cancellation flag in ExecutionCancellationContext.
  4. If the macro implements isCancellable() and handles the signal, it stops promptly.
  5. If the macro does not support cancellation, the interrupt and flag are issued, but the macro continues until it finishes or times out naturally.

Override isCancellable() to declare graceful cancellation support.

1
2
3
4
5
@Override
public boolean isCancellable() {
    return true;
}

Cancellation is also a contract. If a macro returns true but ignores cancellation signals, administrators may believe the operation stopped while the macro continues consuming worker resources.

Your macro must handle both signals sent by the framework.

Handle thread interruption

Thread interruption is used for blocking operations such as sleep and blocking I/O.

1
2
3
4
5
6
7
8
try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    rollbackPartialWork();
    return "<p>Execution cancelled.</p>";
}

Never swallow InterruptedException. Always restore the interrupt status with Thread.currentThread().interrupt(). Swallowing the exception hides the signal from the rest of the call stack and can break cancellation handling.

Check the cancellation flag

Use the cancellation flag for CPU-bound loops and other non-blocking code.

1
2
3
4
5
6
7
8
9
10
import com.atlassian.macro.async.execution.cancellation.ExecutionCancellationContextThreadLocal;

for (Item item : itemsToProcess) {
    if (ExecutionCancellationContextThreadLocal.isCancelled()) {
        cleanupAndRollback();
        return "<p>Execution cancelled.</p>";
    }
    // Process the item.
}

ExecutionCancellationContextThreadLocal.isCancelled() is safe to call from any execution thread. When called outside an executor thread, it returns false rather than throwing an exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public String execute(Map<String, String> params,
                      String body,
                      ConversionContext context)
        throws MacroExecutionException {
    for (Item item : getItems(params)) {
        if (ExecutionCancellationContextThreadLocal.isCancelled()) {
            return "<p>Execution cancelled.</p>";
        }
        try {
            processItem(item);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "<p>Execution cancelled.</p>";
        }
    }
    return renderResult();
}

Implement result caching

Override isCacheable() to opt into the macro result cache.

1
2
3
4
5
@Override
public boolean isCacheable() {
    return true;
}

Cache key semantics

Results are keyed by macroParameters, a hash of the macro body, and userId.

  1. Results are never shared across users. Each user has a separate cache entry.
  2. Different parameters produce different cache entries.
  3. Changes to the macro body invalidate the cache entry.

The cache key does not include the page or space. Identical markup rendered in different contexts can therefore share a cache entry. If output varies by anything outside the parameters and body, do not enable caching.

Checks before enabling caching

CheckWhy it matters
Output depends only on parameters, body, and current userExternal state such as database contents, current time, or permissions may make cached output incorrect.
Output can be up to 60 seconds staleThe default TTL is 60 seconds. Adjust atlassian.macros.result.cache.expire.after.write.millis if required.
The macro has no per-view side effectsCache hits skip execution, so side effects do not run on every page view.
Output does not contain inappropriate sensitive dataThe cache is per user, but verify that external user-specific data is safe to include.

The cache is node-local. In a clustered deployment, users accessing different nodes have independent cache entries and may see different levels of staleness.

Configuration validation

When an administrator configures a macro through the REST API, MacroExecutionControlsValidator applies these rules:

  1. The macro name must not be blank.
  2. The macro must be registered with Confluence's MacroManager. The hosting plugin must be active.
  3. rateLimit, when provided, must be a positive integer. Zero and negative values are rejected.
  4. timeLimit, when provided, must be a positive integer. Zero and negative values are rejected.
  5. Recursive macros, including blog-posts, include, and excerpt-include, cannot use deduplication: true.

If the macro plugin is disabled, an administrator cannot configure it for asynchronous execution. The plugin must be active when configuration is saved.

Known limitations

The following limitations apply in Confluence 11.0. Solutions are planned for future releases.

  • The execution key omits rendering context. It currently contains the macro name, parameters, body hash, and user key, but not the page or space.
  • Macros cannot contribute additional key material. Output that varies because of state outside the parameters is not distinguished during caching or deduplication.
  • Recursive macros are excluded from deduplication by a fixed Confluence list that apps cannot extend.
  • Callers of XhtmlContent cannot force synchronous execution for an individual conversion. disableAsyncRenderSafe() is not consulted before dispatch. If a third-party macro declares itself safe and renders content containing your macros, or invokes your storage-to-view transformers, your code may run on a worker thread even if you have not opted in. Nothing your macro declares prevents this. A future ConversionContext property is intended to allow a transformer registered early in the chain to suppress asynchronous dispatch for conversions it does not own.
  • Macro identity is scoped to the executing thread. A ConversionContext created during macro execution reports the enclosing macro identity.
  • The execution key retains a live reference to the parameter map passed to execute(). Mutating the map after registration can change the key.
  • Cleanup restores macroName only and may leave macroDefinition and macroMetadata at their last written values.
  • Confluence macros do not currently declare asynchronous execution or deduplication support.

Rate this page: