temporalio/temporal · error

reader with ID %v already exists

Error message

reader with ID %v already exists

What it means

ReaderGroup.newReaderLocked panics when a reader with the same numeric ID already exists in the group's readerMap. Reader IDs are unique keys for in-memory readers; GetOrCreateReader guards with a map lookup, so hitting this panic means a race or a bug bypassed the check — e.g. NewReader was called directly with a taken ID, or two goroutines raced without the group lock.

Source

Thrown at service/history/queues/reader_group.go:129

		return nil, false
	}

	reader, ok := g.readerMap[readerID]
	return reader, ok
}

func (g *ReaderGroup) NewReader(readerID int64, slices ...Slice) Reader {
	g.Lock()
	defer g.Unlock()

	return g.newReaderLocked(readerID, slices...)
}

func (g *ReaderGroup) newReaderLocked(readerID int64, slices ...Slice) Reader {
	reader := g.initializer(readerID, slices)

	if _, ok := g.readerMap[readerID]; ok {
		panic(fmt.Sprintf("reader with ID %v already exists", readerID))
	}

	g.readerMap[readerID] = reader

	if g.isStarted() {
		reader.Start()
	}
	return reader
}

func (g *ReaderGroup) RemoveReader(readerID int64) {
	g.Lock()
	defer g.Unlock()

	reader, ok := g.readerMap[readerID]
	if !ok {
		return
	}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Use GetOrCreateReader instead of NewReader so existing readers are returned rather than duplicated
  2. Ensure all reader creation goes through the ReaderGroup under its lock; never hold a stale reader reference that wasn't closed
  3. Check the reader lifecycle: confirm the reader with that ID was closed and removed from the group before reusing the ID

Example fix

// before
reader := group.NewReader(readerID, slices...) // panics if ID exists

// after
reader := group.GetOrCreateReader(readerID, slices...)
Defensive patterns

Strategy: validation

Validate before calling

// use the group API instead of creating directly
if r := group.GetReader(readerID); r == nil {
  r = group.GetOrCreateReader(readerID, slices...)
}

Prevention

When it happens

Trigger: Calling ReaderGroup.NewReader(id, ...) with an ID that already exists; a race where GetOrCreateReader's locked check and creation interleave incorrectly; deterministic ID generation colliding after reader recycling.

Common situations: Custom code paths calling NewReader instead of GetOrCreateReader; ID reuse after a reader was expected to be closed but wasn't removed from the map; tests constructing multiple readers with constant IDs.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/77a3ced06c4d1fd2. Report an issue: GitHub.