Custom field writes take 25–45s when the field's expression references

Raised with Developer Support as ECOHELP-151866.

Posting here because this took days to track down and I would rather nobody else spent them.

Writing a value to a jira:customFieldType field takes 25–45 seconds instead of sub-second, if the module’s formatter.expression or validation.expression references configuration.

Why this needed a second app to find

Our saves had been slow for a long time and everything we suspected turned out to be innocent.

First we blamed our own code. The write goes through a resolver, an async queue and a consumer, so we added a button that called a bare view.submit with a single numeric property — no resolver, no queue, no REST calls of ours anywhere. Still ~34 s. Then we suspected the bridge, and measured the
same write through view.submit, through requestJira, and through a plain browser fetch fromthe DevTools console. All three identical. Then the issue itself — but writing summary on that very same issue took 650 ms.

At that point every remaining suspect lived in the manifest, and a shipped app has far too many of them to test by reasoning. Twenty modules, eight scopes, triggers, entity properties, licensing, two custom field types. So we built a throwaway app with one module and nothing else, confirmed it was fast, and then added the real field’s features to it one at a time, redeploying and re-measuring after each.

Each step below changes exactly one thing.

Step 1 — object field, six properties (three number, three string), each with a
searchAlias. Expressions that do not touch configuration:

        validation:
          expression: |
            value == null || (value.amountA == null || value.amountA >= 0)
        formatter:
          expression: |
            value == null ? "" : (value.labelA != null ? value.labelA : "-")
          export: true

634 / 697 / 667 ms

Step 2 — add contextConfig to the module. Nothing else changed.

645 / 728 / 828 ms. No change. Making the field context-configurable is free.

Step 3 — read configuration from the expressions. Schema, searchAlias, contextConfig, issue and transport all identical to step 2:

        validation:
          expression: |
            let cfg = configuration != null ? configuration : {};
            let max = cfg.maxAmount != null ? cfg.maxAmount : 100000;
            value == null || (value.amountA == null || (value.amountA >= 0 && value.amountA <= max))
        formatter:
          expression: |
            let cfg = configuration != null ? configuration : {};
            let unit = cfg.unit != null ? cfg.unit : "u";
            value == null ? "" : (value.amountA != null ? value.amountA + unit : "")
          export: true

24 867 / 32 393 ms

That is the whole finding. One reference, 40×.

No configuration was ever saved for the field’s context, so both expressions fall through to their defaults. The field is on one project. Every slow response is HTTP 204 on the first attempt — no errors, no retries, no rate limiting, just a successful write that takes half a minute. Variance is around ±25% run to run, which argues against a fixed timeout.

The same in the shipped app

write result
our field, single property 25.7 – 31.6 s
a second, independent field of the same type 24.3 – 30.3 s
summary (native Jira field), same issue 544 – 724 ms

A simpler field of ours with two properties, referencing configuration in the formatter but not in the validation, sits at 11–14 s — about half. Soft evidence that both expressions contribute.

Things we eliminated along the way, each by measurement rather than argument: our backend code · transport · the issue and project · issue properties and jira:entityProperty extraction · app.licensing.enabled · contextConfig on its own · searchAlias and JQL indexing (removing all six cut 34 s to 24–29 s, so real but only ~25%) · number of schema properties · a single corrupted field instance.

The detail that stings

Our validation started with:

let cfg = configuration != null ? configuration : {};

…and never read cfg on any code path. Left over from an earlier version of the expression.

A no-op reference to a documented expression variable, costing 40× on every write, is not something an app author can discover — and I could not find it mentioned anywhere in the docs.

The fix

We removed every configuration reference from the expressions.

The validation reference was dead code and simply went away. The formatter now reads display strings that our app already stores inside the field value, having formatted them with that same configuration at write time — the configuration is applied once, when writing, instead of on every read and write by the platform.

Saves went from 25–45 s to XXX ms. Same field, same issue, same everything else.

The cost is that one configuration option, which only the formatter ever honoured, no longer has any effect; we would have to bake it into the stored string instead. And an app whose rendering genuinely depends on per-context configuration has no equivalent escape — which is why this is also open as ECOHELP-151866 rather than just a note to self.

Hi @MichaKrysiuk, the docs are worse than silent here. The ## Example on the jira:customFieldType page is a progress bar that reads configuration in the formatter, the validation and the parser:

      validation:
        expression: value == null || value >= (configuration?.minValue || 0) && value <= (configuration?.maxValue || 100)

The ### Configuration example higher up the same page does it again. You measured two of those three slots, and anyone building a configurable field straight off that page reproduces your 25-45s without doing anything unusual.

My read is that it lands harder in ECOHELP-151866 than a missing warning would, since Atlassian can check it against their own page.