Repository navigation
Resend rate-limited receipts with backoff, but never after resolution - #97
Merged
Merged
Conversation
eivindjonassen
force-pushed
the
fix/receipt-backoff
branch
2 times, most recently
from
November 1, 2026 20:57
67c41b0 to
f15809e
Compare
A MessageRateExceeded receipt called markRetry, which only updated queued rows, so the ticketed row was checked again on every tick and never resent. markRetry now requeues ticketed rows with the ticket backoff, without counting the send twice. Resolving an interaction now also marks its ticketed rows RESOLVED and clears their push data. They keep their receipt check, so DeviceNotRegistered still disables the device, but markRetry fails them instead of resending a notification for a resolved interaction.
eivindjonassen
force-pushed
the
fix/receipt-backoff
branch
from
November 1, 2026 21:09
5335adf to
6fe2cca
Compare
This was referenced Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bug. A
MessageRateExceededreceipt calledmarkRetry, butmarkRetryonly updated rows still inqueued. The row wasticketed, so nothing changed. It stayed ticketed, its receipt was checked again on every tick, and the message was never resent. This was found while adding tests in #88. A test locked in that behavior, and this PR replaces it.Fix.
markRetryalso acceptsticketedrows. It moves them back toqueuedwithnext_attempt_at_ms = now + 2^min(attempts, 8) s(capped at 15 min), clearsticket_idandreceipt_due_at_ms, and recordsMESSAGE_RATE_EXCEEDED.markTicketedalready counts the send, so a ticketed row keeps its count, and the receipt backoff matches the ticket backoff (2 s, then 4 s, …). While delivery is paused, rows still fail withPAUSED. All other outcomes are unchanged.A gap this would have opened. Resolution only failed
queuedrows. A sent row waits up to 15 minutes for its receipt. If the user answered the permission in that window and the receipt came back rate-limited, the fix above would have resent a notification for something already resolved. So resolving an interaction now also marks itsticketedrowslast_error_code = 'RESOLVED'and clears their push data, as failed rows get. They stayticketed, so the receipt is still checked:DeviceNotRegisteredfailed / DEVICE_NOT_REGISTEREDMessageRateExceededfailed / RESOLVED, nothing resentfailed / EXPO_RECEIPT_ERRORA queued row can't carry
RESOLVED, because resolving a queued row fails it right away, so the mark is unambiguous. Interactions resolved while a send request is in flight were already safe, and a test now covers that.No schema change. Rate limits still have no attempt limit, for tickets or receipts, as before. Adding one would be a separate change to
markRetry.Verification
main: rate-limited receipts get requeued with the ticket backoff, they're resent after it and back off again, andmarkRetryrequeues ticketed rows without counting the send twice. This one failed before the resolution guard: no resend for resolved interactions. New guards that passed before and still pass:DeviceNotRegisteredfor resolved rows, interactions resolved during the send, and rate limits while paused.pnpm lintacceptPluginEvent10.0,markRetry5.0,applyReceipt4.0