aeron-io/aeron · error · TimeoutException

failed to list remote recording descriptor

Error message

failed to list remote recording descriptor

What it means

ReplicationSession throws this TimeoutException during the initial phase of replication: it polls the source archive for the recording descriptor (RecordingDescriptorPoller) and if no descriptor arrives within actionTimeoutMs of timeOfLastActionMs, it gives up. Replication cannot proceed without the remote descriptor, which supplies the recording identity and positions needed to set up the replays that transfer the data.

Solutions

  1. Verify the source recordingId exists on the source archive (list recordings via AeronArchive.listRecording) before replicating
  2. Check connectivity between the two archives: control channels, name resolution and firewall rules for the archive control endpoints
  3. Increase the destination archive's action timeout (actionTimeoutMs / replication timeout config) if the source is slow but reachable
  4. Retry the replication once the source archive is healthy; add application-level retry around AeronArchive.replicate with backoff

Example fix

// before: no existence check, no retry
archive.replicate(recordingId, dstRecordingId, srcControlChannel, srcReplicationChannel);

// after: verify the descriptor exists, then replicate
if (archive.listRecording(recordingId) == null) {
    throw new IllegalStateException("recording not found on source: " + recordingId);
}
archive.replicate(recordingId, dstRecordingId, srcControlChannel, srcReplicationChannel);
Defensive patterns

Strategy: retry

Validate before calling

try (AeronArchive probe = AeronArchive.connect(new AeronArchive.Context().controlChannel(srcControlChannel))) {
    if (probe.listRecording(sourceRecordingId) == null) {
        throw new IllegalArgumentException("recording not found on source archive: " + sourceRecordingId);
    }
}

Type guard

static boolean sourceArchiveReachable(String controlChannel, long timeoutMs) {
    try (AeronArchive a = AeronArchive.connect(new AeronArchive.Context().controlChannel(controlChannel))) {
        return a.archiveProxy().timeOfLastStatusMessage() > 0 || a.archiveClientProxy() != null;
    } catch (Exception e) { return false; }
}

Try / catch

try {
    replicationId = archive.replicate(recordingId, dstRecordingId, srcControlChannel, srcReplicationChannel);
} catch (TimeoutException e) {
    if (e.getMessage().contains("failed to list remote recording descriptor")) {
        backoffAndRetryReplication(recordingId, dstRecordingId, maxAttempts); // check source connectivity/recordingId between attempts
    } else { throw e; }
}

Prevention

When it happens

Trigger: replicateDescriptor (driven from doWork) polls srcArchive.recordingDescriptorPoller() but receives no matching descriptor fragment before epochClock.time() >= timeOfLastActionMs + actionTimeoutMs. Causes: source archive unreachable or slow, wrong recordingId queried on the source, control-request/response channel congestion, source archive overloaded, or the archive's default timeout too small for a slow remote.

Common situations: Network partitions or high latency between source and destination archives; requesting replication of a recordingId that does not exist (or was already purged) on the source archive; firewalls blocking the control channel so the response never arrives; a destination archive configured with an actionTimeoutMs shorter than the source's response time under load.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of aeron-io/aeron@6d60124e15 (2026-09-12). Data as JSON: /api/errors/823160e2ab258492. Report an issue: GitHub.

Appendix: source

Thrown at aeron-archive/src/main/java/io/aeron/archive/ReplicationSession.java:458

        return workCount;
    }

    private int replicateDescriptor()
    {
        int workCount = 0;

        if (NULL_VALUE == activeCorrelationId)
        {
            final long correlationId = aeron.nextCorrelationId();
            if (srcArchive.archiveProxy().listRecording(srcRecordingId, correlationId, srcArchive.controlSessionId()))
            {
                workCount += trackAction(correlationId);
                srcArchive.recordingDescriptorPoller().reset(correlationId, 1, this);
            }
            else if (epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))
            {
                throw new TimeoutException("failed to list remote recording descriptor");
            }
        }
        else
        {
            final RecordingDescriptorPoller poller = srcArchive.recordingDescriptorPoller();
            final int fragments = poller.poll();

            if (poller.isDispatchComplete() && poller.remainingRecordCount() > 0)
            {
                final String errorMsg = "unknown src recording id " + srcRecordingId;
                state(State.DONE, errorMsg);
                error(errorMsg, ArchiveException.UNKNOWN_RECORDING);
            }

            if (0 == fragments && epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))
            {
                throw new TimeoutException("failed to fetch remote recording descriptor");
            }

View on GitHub (pinned to 6d60124e15)