antiwork/gumroad · warning · Installment::InstallmentInvalid

This email is already being sent. Please wait a few minutes

Error message

This email is already being sent. Please wait a few minutes before trying again.

What it means

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.

Source

Thrown at app/controllers/api/internal/customers/single_customer_emails_controller.rb:144

          ensure_delivery_recorded!(installment, purchase)
        elsif !delivery_recorded?(installment, purchase)
          Rails.cache.delete(cache_key)
        end
        raise
      end
    end

    def delivery_already_sent_or_reserved!(cache_key, installment, purchase)
      with_redis_lock("#{cache_key}:lock") do
        cache_value = Rails.cache.read(cache_key)
        if [DELIVERY_SENT_CACHE_VALUE, true].include?(cache_value)
          ensure_delivery_recorded!(installment, purchase)
          :sent
        elsif delivery_recorded?(installment, purchase)
          mark_delivery_sent(cache_key)
          :sent
        elsif cache_value == DELIVERY_IN_PROGRESS_CACHE_VALUE
          raise Installment::InstallmentInvalid, "This email is already being sent. Please wait a few minutes before trying again."
        else
          Rails.cache.write(cache_key, DELIVERY_IN_PROGRESS_CACHE_VALUE, expires_in: DELIVERY_IN_PROGRESS_CACHE_TTL)
          :reserved
        end
      end
    end

    def mark_delivery_sent(cache_key)
      Rails.cache.write(cache_key, DELIVERY_SENT_CACHE_VALUE, expires_in: DELIVERY_CACHE_TTL)
      true
    rescue StandardError => e
      Rails.logger.warn("Failed to write single-customer email delivery cache #{cache_key}: #{e.class}: #{e.message}")
      false
    end

    def delivery_sent_cache?(cache_key)
      Rails.cache.read(cache_key) == DELIVERY_SENT_CACHE_VALUE
    rescue StandardError

View on GitHub (pinned to afeacbd394)

Solutions

  1. Wait a few minutes (the in-progress TTL) and retry the send — if the first attempt completed, the retry returns :sent without resending.
  2. 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.
  3. Fix the client: disable the send button while a request is in flight and don't auto-retry non-idempotent sends.
  4. Operators: if a crashed worker left the marker stuck, wait out the TTL rather than manually deleting the key, or verify delivery first.

Example fix

// before: fire-and-forget double submit
sendButton.onclick = () => api.post(`/single_customer_emails`, payload)
// after: in-flight guard so the second click never reaches the server
let sending = false
sendButton.onclick = async () => {
  if (sending) return
  sending = true
  try { await api.post(`/single_customer_emails`, payload) } finally { sending = false }
}
Defensive patterns

Strategy: retry

Validate before calling

# client: disable duplicate sends while one is in flight
if (cache = Rails.cache.read(key)) && ["sent", true, "in_progress"].include?(cache.to_s)
  return :already_handled
end

Try / catch

begin
  Posts::SingleCustomerEmail.create!(...)
rescue Installment::InstallmentInvalid => e
  retry_after(minutes: 3) if e.message.include?("already being sent")
end

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/89b73a1879740d25. Report an issue: GitHub.