microsoft/aspire · error · InvalidOperationException

Circular loop detected for span

Error message

Circular loop detected for span '{span.SpanId}' with parent '{span.ParentSpanId}'.

What it means

OtlpTrace.AddSpan validates parent/child relationships and throws this InvalidOperationException when a newly inserted span creates a circular parent chain (a span is its own ancestor). Circular references would cause infinite loops or broken tree rendering, so the span is removed from Spans and the error is thrown. This guards against malformed trace data where ParentSpanId points back into the span's own ancestry.

Solutions

  1. Fix the emitting instrumentation so ParentSpanId always refers to an ancestor, never the span itself or its descendant.
  2. Validate span graphs before import: detect and break cycles by nulling the offending ParentSpanId.
  3. If the producer is a custom exporter/library, verify it copies the correct parent context (Activity.ParentSpanId / parent trace flags) when creating spans.

Example fix

// before: self-referential parent
var span = new OtlpSpan { SpanId = id, ParentSpanId = id };
// after: parent must be a different, pre-existing span (or empty for a root)
var span = new OtlpSpan { SpanId = id, ParentSpanId = parentId != id ? parentId : null };
Defensive patterns

Strategy: validation

Validate before calling

// verify the parent chain is acyclic before adding
bool isSelfParent = span.SpanId == span.ParentSpanId;
bool parentExists = string.IsNullOrEmpty(span.ParentSpanId) || trace.Spans.Any(s => s.SpanId == span.ParentSpanId);

Try / catch

try
{
    trace.AddSpan(span);
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Circular loop detected"))
{
    logger.LogWarning(ex, "Rejecting span {SpanId} with cyclic parent {ParentId}.", span.SpanId, span.ParentSpanId);
}

Prevention

When it happens

Trigger: AddSpan throws when HasCircularReference(span) is true after inserting the span, i.e. span.ParentSpanId resolves (directly or transitively) back to span.SpanId. Caused by telemetry producers emitting inconsistent parent ids, e.g. a span listing itself as parent, or A/B cycles from buggy context propagation.

Common situations: Buggy custom context propagation overwriting parent ids; span re-parenting logic in middleware producing a cycle; test fixtures hand-crafting span graphs with wrong parent ids; producer bugs after sampling/context corruption in async flows.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/26bba7996f208b4c. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Dashboard/Otlp/Model/OtlpTrace.cs:61

        {
            throw new InvalidOperationException($"Duplicate span id '{span.SpanId}' detected.");
        }

        var insertIndex = 0;
        for (var i = Spans.Count - 1; i >= 0; i--)
        {
            if (span.StartTime > Spans[i].StartTime)
            {
                insertIndex = i + 1;
                break;
            }
        }
        Spans.Insert(insertIndex, span);

        if (HasCircularReference(span))
        {
            Spans.Remove(span);
            throw new InvalidOperationException($"Circular loop detected for span '{span.SpanId}' with parent '{span.ParentSpanId}'.");
        }

        _duration = null;

        if (string.IsNullOrEmpty(span.ParentSpanId))
        {
            // There should only be one span with no parent span ID.
            // Incase there isn't, the first span with no parent span ID is considered to be the root.
            foreach (var existingSpan in Spans)
            {
                if (string.IsNullOrEmpty(existingSpan.ParentSpanId))
                {
                    _rootSpan = existingSpan;
                    FullName = BuildFullName(existingSpan);
                    break;
                }
            }
        }

View on GitHub (pinned to 25830f84bd)