Skip to content

relayer: submit retry delay reuses relayer_poll_frequency_ms — single config knob controls two distinct timings #60

@Nexory

Description

@Nexory

Current code

src/polymarket/_internal/actions/relayer/gasless.py, lines 73 and 89 (async path); lines 390 and 406 (sync path):

env = ctx.environment
retry_delay_s = env.relayer_poll_frequency_ms / 1000   # line 73 (async) / 390 (sync)
last_error: BaseException | None = None
for attempt in range(GASLESS_SUBMIT_RETRY_ATTEMPTS + 1):
    try:
        response = await _submit_for_wallet_type(ctx, calls=calls, metadata=metadata)
        ...
    except Exception as error:
        last_error = error
        if attempt == GASLESS_SUBMIT_RETRY_ATTEMPTS or not is_retryable_submit_error(error):
            raise
        await asyncio.sleep(retry_delay_s)  # line 89 / 406

relayer_poll_frequency_ms is defined in environments.py:42 with a default of 2000 ms. It was introduced to govern the interval between status-poll calls in poll_until_terminal (poll.py:47). That is its documented purpose.

GASLESS_SUBMIT_RETRY_ATTEMPTS = 10 is defined in submit.py:10.

Reproduction

A caller who wants low-frequency status polling (e.g. to avoid hammering the relayer status endpoint) sets:

env = Environment(relayer_poll_frequency_ms=10_000)

This is a perfectly reasonable tuning of the poll interval. The unintended side effect: every submit retry on a transient error (wallet busy, nonce mismatch, rate-limit 400) now waits 10 seconds per attempt. With GASLESS_SUBMIT_RETRY_ATTEMPTS = 10, the worst-case submit phase grows from 20 s → 100 s, delaying the transaction by over a minute before a terminal error is raised.

The inverse also applies: a caller who wants fast submit retries (e.g. 200 ms) and lowers relayer_poll_frequency_ms accordingly will hammer the status endpoint far more than intended.

Impact

  • Any integrator who tunes relayer_poll_frequency_ms away from the 2000 ms default inadvertently alters submit retry timing with no indication this will happen — the field name gives no hint it controls submit behaviour.
  • In high-contention scenarios (wallet busy, nonce collision) where fast retries matter most, a user who raised poll frequency to reduce status-endpoint load takes a severe latency penalty on the submit phase.
  • No workaround is available without forking the environment model; there is no separate submit-retry knob.

Suggested fix

Option A (preferred): Introduce a dedicated field in the Environment dataclass (environments.py):

relayer_submit_retry_delay_ms: int = 500

Then in prepare_gasless_transaction / prepare_gasless_transaction_sync, derive the retry delay from this field instead:

retry_delay_s = env.relayer_submit_retry_delay_ms / 1000

This decouples the two semantically distinct timings and gives callers independent control. A 500 ms default is fast enough to resolve transient locks without hammering the submit endpoint.

Option B (minimal): Keep the shared field but document the dual role explicitly in the Environment docstring and in prepare_gasless_transaction. At minimum this makes the coupling visible so callers can reason about the tradeoff.

Related

  • environments.py:42relayer_poll_frequency_ms definition (only existing timing knob)
  • submit.py:10GASLESS_SUBMIT_RETRY_ATTEMPTS = 10
  • poll.py:47 — the status-poll loop that relayer_poll_frequency_ms was originally designed to govern

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions