Skip to content

Concurrent /otp signup race for the same new email: losing request returns raw 500 (users_email_partial_key, aborted transaction) instead of a graceful response #2675

Description

@beedabug

What happened

Hosted Supabase project, 2026-08-02 ~02:37 UTC. Two POST /auth/v1/otp requests for the same not-yet-existing email address were in flight concurrently (~300 ms apart; email OTP via supabase-js signInWithOtp, auth-js 2.110.x, shouldCreateUser default). The first request created the auth user and sent the code (audit log shows exactly one user_confirmation_requested). The second hit the partial unique index on email during user creation and surfaced a raw 500:

POST | 500 | /auth/v1/otp
error: "failed to close prepared statement: ERROR: current transaction is aborted, commands ignored
until end of transaction block (SQLSTATE 25P02): ERROR: duplicate key value violates unique
constraint \"users_email_partial_key\" (SQLSTATE 23505)"
error_code: unexpected_failure · duration 327ms

To reproduce (untested sketch)

We have not re-run this against a live project (it creates a throwaway user and sends a real email), but the collision should reproduce with two sufficiently concurrent requests for the same never-before-seen address — the observed race had the requests ~300 ms apart against a winning request that took 327 ms, so the loser must land inside the winner's user-creation transaction:

EMAIL="race-$(date +%s)@your-domain.example"   # fresh address, domain you control
URL="https://<project-ref>.supabase.co/auth/v1/otp"
BODY=$(printf '{"email":"%s","create_user":true}' "$EMAIL")
for i in 1 2; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST "$URL"     -H "apikey: $ANON_KEY" -H "Content-Type: application/json" -d "$BODY" &
done
wait

Observed outcome: one 200, one 500 (unexpected_failure, the error above). Expected: two 200s (loser falls through to the existing-user OTP path, as a retry a second later would) or a structured 4xx.

System information

  • Supabase: hosted (supabase.com); incident 2026-08-02 ~02:37 UTC, so whatever Auth build was deployed to hosted projects at that date
  • Client: supabase-js 2.110.8 (resolved @supabase/auth-js 2.110.8), browser web app
  • Flow: signInWithOtp email OTP, shouldCreateUser default (true), PKCE
  • Endpoint: POST /auth/v1/otp

Why this seems worth hardening server-side

Two concurrent signups for one email is a predictable race (double-submit, multiple forms on one page, network-layer duplication), but the losing request currently fails with unexpected_failure out of an aborted transaction rather than a structured response. A client cannot distinguish this from a genuine server outage. Expected behavior would be a graceful path — e.g. detect the conflict and fall through to the existing-user OTP path (idempotent-ish, matching what a retry a second later would do), or a structured 4xx (409/429).

In our case the user happened to be unaffected (the winning request's success reached the client first), but if the ordering flips, an email+OTP signup flow shows a hard failure at the first step while a valid code is already in the user's inbox.

Notes

  • We ruled out an app-level double submit on our side (single form submit; client instrumented since) — but regardless of where a duplicate originates, the collision handling in the /otp user-creation path is server-side.
  • Client-side error observability for this class was also degraded at the time by the 5xx "{}"-message issue fixed in supabase-js#2587 — worth noting only as context for why such 500s can go unnoticed.
  • Searched open/closed issues here for /otp idempotency, users_email_partial_key, and concurrent-signup races — found no existing report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions