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:42 — relayer_poll_frequency_ms definition (only existing timing knob)
submit.py:10 — GASLESS_SUBMIT_RETRY_ATTEMPTS = 10
poll.py:47 — the status-poll loop that relayer_poll_frequency_ms was originally designed to govern
Current code
src/polymarket/_internal/actions/relayer/gasless.py, lines 73 and 89 (async path); lines 390 and 406 (sync path):relayer_poll_frequency_msis defined inenvironments.py:42with a default of2000ms. It was introduced to govern the interval between status-poll calls inpoll_until_terminal(poll.py:47). That is its documented purpose.GASLESS_SUBMIT_RETRY_ATTEMPTS = 10is defined insubmit.py:10.Reproduction
A caller who wants low-frequency status polling (e.g. to avoid hammering the relayer status endpoint) sets:
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_msaccordingly will hammer the status endpoint far more than intended.Impact
relayer_poll_frequency_msaway 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.Suggested fix
Option A (preferred): Introduce a dedicated field in the
Environmentdataclass (environments.py):Then in
prepare_gasless_transaction/prepare_gasless_transaction_sync, derive the retry delay from this field instead: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
Environmentdocstring and inprepare_gasless_transaction. At minimum this makes the coupling visible so callers can reason about the tradeoff.Related
environments.py:42—relayer_poll_frequency_msdefinition (only existing timing knob)submit.py:10—GASLESS_SUBMIT_RETRY_ATTEMPTS = 10poll.py:47— the status-poll loop thatrelayer_poll_frequency_mswas originally designed to govern