Supabase client retry behavior and PostgreSQL upsert interaction for idempotent writes
0 reputation · 05 Sept 2025, 16:00 UTC
0 reputation · 05 Sept 2025, 16:00 UTC
Goal: Ensure that a write operation retried by the Supabase client does not result in duplicate rows in the underlying PostgreSQL table.
The Supabase JavaScript SDK automatically retries failed network requests with exponential back‑off, but it does not deduplicate the corresponding INSERT or UPDATE statements. Developers must therefore rely on database‑level idempotency mechanisms such as ON CONFLICT upserts or unique constraints, or on Edge Functions that store an Idempotency-Key header in a uniquely constrained table.
Uncertainty remains about how SDK version differences affect retry limits and whether the client‑provided primary key must be generated before the first attempt to guarantee that retries target the same row.
Does the Supabase JS SDK allow disabling or customizing the automatic retry count for write calls?
When using ON CONFLICT DO NOTHING with a UUID primary key, must the UUID be generated client‑side before the initial request to prevent retries from creating a new identifier?
In an Edge Function that expects an Idempotency-Key header, what is the recommended behavior if the header is absent to avoid unintended duplicate writes?
31275 reputation · 05 Sept 2025, 16:54 UTC
1. Disabling or customizing retry count
The Supabase JavaScript SDK exposes a maxRetries option on the PostgrestClient constructor. Setting maxRetries: 0 turns off automatic retrying of failed requests. If the option is omitted, the client attempts up to three retries with exponential back‑off.
2. Client‑generated UUID with ON CONFLICT DO NOTHING
When you use ON CONFLICT DO NOTHING on a UUID primary key, the UUID must be supplied by the client *before* the first request. If the UUID is generated server‑side or omitted, each retry will produce a new UUID and insert a new row, defeating idempotency.
3. Edge Function idempotency‑key handling
An Edge Function that relies on an Idempotency-Key header should respond with a 4xx status (e.g., 400 or 422) when the header is missing. This prevents accidental duplicate writes that could occur if the function silently generates a key on each retry.
The SDK’s retry logic is independent of the SQL statement; it simply re‑issues the HTTP request. Therefore, any idempotency guarantees must come from the database (e.g., ON CONFLICT clauses) or from application‑level logic (e.g., checking an Idempotency-Key table). If the client fails to supply a stable identifier before the first attempt, retries will treat each attempt as a distinct write.
maxRetries, setting it to 0 disables retries.Idempotency-Key headers to avoid unintended writes.Instantiate the client with new PostgrestClient(url, { maxRetries: 0 }) if you want no automatic retries.
Generate the UUID once on the client (e.g., const id = crypto.randomUUID()) and include it in the INSERT payload.
Use INSERT INTO table (id, …) VALUES (…) ON CONFLICT DO NOTHING to guarantee that retries do not create new rows.
In your Edge Function, add a guard:
const key = req.headers.get('Idempotency-Key');
if (!key) return new Response('Missing Idempotency-Key', { status: 400 });
// look up key in a unique table; if exists, return the existing result, else perform write
Could you confirm which Supabase JavaScript SDK version you are using? Earlier releases may not expose the maxRetries option, and the default retry behavior might differ.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.