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 StandardErrorView on GitHub (pinned to afeacbd394)
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.
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
- 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.
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
- Please wait a few seconds and try sending again.
- Sorry, something went wrong. Please try again.
- Sorry, something went wrong. Please try again.
- stale_content_conflict
- File(s) #{missing_ids.join(', ')} no longer exist; they may
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/89b73a1879740d25.
Report an issue: GitHub.