{"record":{"id":"89b73a1879740d25","repo":"antiwork/gumroad","slug":"this-email-is-already-being-sent-please-wait-a-fe","errorCode":null,"errorMessage":"This email is already being sent. Please wait a few minutes before trying again.","messagePattern":"This email is already being sent\\. Please wait a few minutes before trying again\\.","errorType":"exception","errorClass":"Installment::InstallmentInvalid","httpStatus":422,"severity":"warning","filePath":"app/controllers/api/internal/customers/single_customer_emails_controller.rb","lineNumber":144,"sourceCode":"          ensure_delivery_recorded!(installment, purchase)\n        elsif !delivery_recorded?(installment, purchase)\n          Rails.cache.delete(cache_key)\n        end\n        raise\n      end\n    end\n\n    def delivery_already_sent_or_reserved!(cache_key, installment, purchase)\n      with_redis_lock(\"#{cache_key}:lock\") do\n        cache_value = Rails.cache.read(cache_key)\n        if [DELIVERY_SENT_CACHE_VALUE, true].include?(cache_value)\n          ensure_delivery_recorded!(installment, purchase)\n          :sent\n        elsif delivery_recorded?(installment, purchase)\n          mark_delivery_sent(cache_key)\n          :sent\n        elsif cache_value == DELIVERY_IN_PROGRESS_CACHE_VALUE\n          raise Installment::InstallmentInvalid, \"This email is already being sent. Please wait a few minutes before trying again.\"\n        else\n          Rails.cache.write(cache_key, DELIVERY_IN_PROGRESS_CACHE_VALUE, expires_in: DELIVERY_IN_PROGRESS_CACHE_TTL)\n          :reserved\n        end\n      end\n    end\n\n    def mark_delivery_sent(cache_key)\n      Rails.cache.write(cache_key, DELIVERY_SENT_CACHE_VALUE, expires_in: DELIVERY_CACHE_TTL)\n      true\n    rescue StandardError => e\n      Rails.logger.warn(\"Failed to write single-customer email delivery cache #{cache_key}: #{e.class}: #{e.message}\")\n      false\n    end\n\n    def delivery_sent_cache?(cache_key)\n      Rails.cache.read(cache_key) == DELIVERY_SENT_CACHE_VALUE\n    rescue StandardError","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/internal/customers/single_customer_emails_controller.rb#L126-L162","documentation":"Installment::InstallmentInvalid raised by Api::Internal::Customers::SingleCustomerEmailsController#delivery_already_sent_or_reserved! while holding the per-(installment,purchase) Redis lock. The delivery cache key is in DELIVERY_IN_PROGRESS state (set with a TTL when a previous request reserved the send), so a concurrent or immediately-repeated send of the same single-customer email is refused instead of double-sending. It is dedupe infrastructure, not a data problem: the first send is still in flight.","triggerScenarios":"POSTing a single-customer email send for the same installment+purchase twice in quick succession: the first request set cache value DELIVERY_IN_PROGRESS_CACHE_VALUE, and the second sees that value under the lock and raises. Also happens when the first request crashed mid-send and the in-progress marker outlives it (bounded by DELIVERY_IN_PROGRESS_CACHE_TTL).","commonSituations":"Double-click on the send button; UI retry logic firing while the first request is slow; background job and manual admin send racing; leftover in-progress marker after a worker crash, clearing only when the TTL expires.","solutions":["Wait a few minutes (the in-progress TTL) and retry the send — if the first attempt completed, the retry returns :sent without resending.","Check whether the email actually went out (delivery_recorded?/ensure_delivery_recorded!) before retrying, so you don't assume a failure that was a success.","Fix the client: disable the send button while a request is in flight and don't auto-retry non-idempotent sends.","Operators: if a crashed worker left the marker stuck, wait out the TTL rather than manually deleting the key, or verify delivery first."],"exampleFix":"// before: fire-and-forget double submit\nsendButton.onclick = () => api.post(`/single_customer_emails`, payload)\n// after: in-flight guard so the second click never reaches the server\nlet sending = false\nsendButton.onclick = async () => {\n  if (sending) return\n  sending = true\n  try { await api.post(`/single_customer_emails`, payload) } finally { sending = false }\n}","handlingStrategy":"retry","validationCode":"# client: disable duplicate sends while one is in flight\nif (cache = Rails.cache.read(key)) && [\"sent\", true, \"in_progress\"].include?(cache.to_s)\n  return :already_handled\nend","typeGuard":null,"tryCatchPattern":"begin\n  Posts::SingleCustomerEmail.create!(...)\nrescue Installment::InstallmentInvalid => e\n  retry_after(minutes: 3) if e.message.include?(\"already being sent\")\nend","preventionTips":["Disable the send control while a request is in flight; never auto-retry non-idempotent sends.","Check delivery state (sent marker) before allowing a manual resend.","Bound retries to a couple with backoff spanning the in-progress TTL."],"tags":["email","dedupe","redis","concurrency","rate-limiting"],"backgroundTag":"duplicate-request-in-flight","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}