SigNoz/signoz · warning · model.ApiError

query %s already started

Error message

query %s already started

What it means

BadRequest from ReportQueryStarted on the in-memory query progress tracker when a queryId is already registered and not yet finished. The tracker keys live queries by ID; registering a duplicate ID is rejected to prevent overwriting progress state.

Source

Thrown at pkg/query-service/app/clickhouseReader/query_progress/inmemory_tracker.go:29

	"golang.org/x/exp/maps"
)

// tracks progress and manages subscriptions for all queries
type inMemoryQueryProgressTracker struct {
	logger  *slog.Logger
	queries map[string]*queryTracker
	lock    sync.RWMutex
}

func (tracker *inMemoryQueryProgressTracker) ReportQueryStarted(
	queryId string,
) (postQueryCleanup func(), apiErr *model.ApiError) {
	tracker.lock.Lock()
	defer tracker.lock.Unlock()

	_, exists := tracker.queries[queryId]
	if exists {
		return nil, model.BadRequest(fmt.Errorf(
			"query %s already started", queryId,
		))
	}

	tracker.queries[queryId] = newQueryTracker(tracker.logger, queryId)

	return func() {
		tracker.onQueryFinished(queryId)
	}, nil
}

func (tracker *inMemoryQueryProgressTracker) ReportQueryProgress(
	queryId string, chProgress *clickhouse.Progress,
) *model.ApiError {
	queryTracker, err := tracker.getQueryTracker(queryId)
	if err != nil {
		return err
	}

View on GitHub (pinned to 5069bf80b0)

Solutions

  1. Generate a fresh unique queryId (uuid) for every query start instead of reusing one
  2. Ensure the returned postQueryCleanup is always called (defer) so IDs are freed
  3. If retrying after a failure, only retry start when the previous ID was cleaned up
  4. Treat this as a client bug: check exists-map before reporting start in wrappers

Example fix

// before
id := "my-fixed-query-id"
cleanup, apiErr := tracker.ReportQueryStarted(ctx, id)

// after
id := uuid.NewString() // unique per attempt
cleanup, apiErr := tracker.ReportQueryStarted(ctx, id)
if cleanup != nil { defer cleanup() }
Defensive patterns

Strategy: validation

Validate before calling

// Always generate a unique id per attempt
queryId := uuid.NewString()

Type guard

func isDuplicateQueryStart(apiErr *model.ApiError) bool {
    return apiErr != nil && apiErr.Typ == model.ErrorBadRequest && strings.Contains(apiErr.Err.Error(), "already started")
}

Try / catch

cleanup, apiErr := tracker.ReportQueryStarted(ctx, queryId)
if isDuplicateQueryStart(apiErr) {
    queryId = uuid.NewString() // regenerate and retry once
    cleanup, apiErr = tracker.ReportQueryStarted(ctx, queryId)
}
if cleanup != nil { defer cleanup() }

Prevention

When it happens

Trigger: Calling ReportQueryStarted (via ReportQueryStartForProgressTracking) twice with the same queryId before the query finishes — e.g. retrying a query initiation with a reused/generated-collision ID, or a code path that reports start more than once.

Common situations: Custom integrations that reuse request IDs for query progress, retries that don't regenerate the queryId, or races where a previous query's cleanup (postQueryCleanup) was never invoked.

Related errors


AI-assisted analysis of SigNoz/signoz@5069bf80b0 (2026-08-28). Data as JSON: /api/errors/c4f89a0305d5e4f8. Report an issue: GitHub.