Repository navigation
feat(js): support async callbacks in group() - #6341
Merged
Merged
Conversation
mstoykov
requested review from
inancgumus and
szkiba
and removed request for
a team
August 26, 2026 12:59
6 of 8 tasks
mstoykov
temporarily deployed
to
azure-trusted-signing
August 26, 2026 13:05 — with
GitHub Actions
Inactive
mstoykov
temporarily deployed
to
azure-trusted-signing
August 26, 2026 13:06 — with
GitHub Actions
Inactive
inancgumus
previously approved these changes
Sep 11, 2026
When async-metric-context is enabled, allow group() callbacks to return Promise-like values. Keep the group's complete metric context across await continuations and promise reactions, and emit group_duration once when the result settles. Restore the parent context after the callback returns or throws. Handle throwing then accessors and custom thenables that settle synchronously, preserve rejection identity, and prevent duplicate duration emission. Disabled runs retain the previous async-function error and synchronous tag behavior. Synchronous callbacks continue to return their original values without entering the asynchronous settlement path. Five one-second runs with GOMAXPROCS=1 measured an awaited resolved promise at 4.934 us/op, 5,376 B/op, and 89 allocations with the feature enabled. The disabled path measured 4.828 us/op, 5,328 B/op, and 87 allocations. Timing ranges overlapped, while the allocation difference was stable. Returning a group around a resolved promise measured 8.796 us/op, 8,424 B/op, and 135 allocations. Awaiting the group measured 11.711 us/op, 10,584 B/op, and 173 allocations.
mstoykov
force-pushed
the
async-group-base
branch
from
September 15, 2026 07:53
fcb0098 to
9eb1c67
Compare
mstoykov
temporarily deployed
to
azure-trusted-signing
September 15, 2026 07:59 — with
GitHub Actions
Inactive
mstoykov
temporarily deployed
to
azure-trusted-signing
September 15, 2026 08:01 — with
GitHub Actions
Inactive
szkiba
approved these changes
Sep 15, 2026
| } | ||
|
|
||
| func asyncGroupsEnabled(vu modules.VU) bool { | ||
| if state := vu.State(); state != nil { |
Contributor
There was a problem hiding this comment.
nit: nil check is obsolete :)
| if common.IsAsyncFunction(mi.vu.Runtime(), val) { | ||
| if !mi.asyncGroups && common.IsAsyncFunction(mi.vu.Runtime(), val) { | ||
| return sobek.Undefined(), errors.New("group() does not support async functions as arguments, " + | ||
| "please see https://grafana.com/docs/k6/latest/javascript-api/k6/group/ for more info") |
Contributor
There was a problem hiding this comment.
Could update this error message to explain that there is a feature flag now
janHildebrandt98
approved these changes
Sep 15, 2026
2 of 4 tasks
inancgumus
added a commit
to grafana/k6-docs
that referenced
this pull request
Sep 15, 2026
Document how to enable asynchronous groups and keep metrics tied to their operation across promises, timers, and protocol callbacks. Related: grafana/k6#6340 Related: grafana/k6#6341 Related: grafana/k6#6342 Related: grafana/k6#6343 Related: grafana/k6#6344
4 of 5 tasks
4 of 5 tasks
mstoykov
added a commit
that referenced
this pull request
Sep 17, 2026
…rror (#6408) When group() is called with an async callback and the async-metric-context experimental feature is not enabled, the returned error now tells the user how to enable it (--features async-metric-context / K6_FEATURES) instead of implying async callbacks are unsupported outright. Addresses review feedback on #6341. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
When
async-metric-contextis enabled, this PR allowsgroup()callbacks to return Promises orPromise-like thenables. The group's complete metric context follows Promise reactions, and
group_durationis emitted once when the returned asynchronous result settles.group()still invokes its callback immediately. A non-thenable result follows the synchronous pathand is returned unchanged. For an asynchronous result, the implementation:
throwing custom thenables.
The disabled path retains the existing rejection of native async callbacks and the existing mutable
tag behavior for synchronous callbacks.
This deliberately makes synchronous and asynchronous groups use the same duration context when the
feature is enabled.
group_durationuses the tags and metadata captured at group entry. Mutationsmade inside the callback affect metrics emitted there, but they do not retag
group_durationand thecomplete parent context is restored when
group()returns. Without the feature, the legacysynchronous path records
group_durationwith the context present when the callback finishes andleaves non-group tag and metadata mutations live.
Five one-second benchmark runs with
GOMAXPROCS=1measured an awaited resolved Promise at4.934 us/op, 5,376 B/op, and 89 allocations with the feature enabled, versus 4.828 us/op,
5,328 B/op, and 87 allocations with it disabled. Returning a group around a resolved Promise
measured 8.796 us/op, 8,424 B/op, and 135 allocations; awaiting it measured 11.711 us/op,
10,584 B/op, and 173 allocations.
Why?
Today,
group()restores its tag as soon as the callback returns. Work after anawaitor inside aPromise handler therefore loses the group, and
group_durationmeasures only the synchronous prefix.That makes groups unreliable for k6's increasingly asynchronous APIs, as reported in #2728 and
summarized in #5435 and #5673.
Duration ownership is kept separate from the general context tracker. The tracker answers which
metric context a Promise reaction should run under; each
group()invocation owns its own start time,entry snapshot, and settlement state. This separation is what lets several groups overlap without one
group completing or retagging another.
Using the entry snapshot for
group_durationalso keeps its attribution independent of when acallback mutates tags or metadata. Applying completion-time context only to synchronous callbacks
would preserve the old behavior under the feature flag, but would make otherwise equivalent sync and
async groups report duration with different contexts.
The callback is intentionally called before checking its return value. This preserves the existing
synchronous
group()behavior and supports ordinary, bound, proxied, or transpiled functions thathappen to return a Promise. It also preserves the execution order
A, C, Bfor a callback that doeswork
A, awaits, then does workB, followed by synchronous caller workC.Alternatives considered:
AsyncFunctionclass and deferred the whole callback through aPromise job. That changes ordering to
C, A, B, misses ordinary functions returning Promises, andcouples group lifetime to async-function tracking rather than the returned value.
group()calls async functions #2848 proposes deferring async callback invocation. This PR deliberately does not adopt thatscheduling change; its regression test locks in immediate invocation. The issue is related but is
not closed by this PR.
finish too early or make
await group(...)resume with the return value ofthen(). Genericthenables are therefore assimilated into a real Promise.
asynchronous operation. A never-settling result consequently has no completed duration, matching
the fact that the group itself never completes.
Checklist
make check) and all pass.Checklist: Documentation (only for k6 maintainers and if relevant)
Please do not merge this PR until the following items are filled out.
k6-documentation: TBD; the
group()documentation mustdescribe the feature flag, asynchronous lifetime, and entry-context semantics of
group_durationTypeScript definitions:
not applicable; the existing generic return type already preserves a Promise returned by the
callback
Related PR(s)/Issue(s)
groupdoesn't work with async calls well (#2728)group()calls async functions (#2848)— related ordering proposal, intentionally not closed
AsyncContextTrackerfor async groups(#3392)
closed
(#5673)