
September 11, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
Payment callbacks arrive twice. A webhook fires again after a timeout. A queue worker crashes mid-job and the broker redelivers the message. These are not edge cases — they are normal behaviour in distributed systems. Understanding Exactly-Once vs At-Least-Once Delivery is how you stop duplicate charges, double bookings, and repeated SMS alerts on production API and integration projects. This guide compares the guarantees, shows where true exactly-once is impossible, and walks through patterns I use on Laravel applications, WooCommerce stores, and legal-tech portals that handle money and documents.
What is the difference between Exactly-Once vs At-Least-Once Delivery?
Message brokers and HTTP integrations promise different delivery semantics. At-least-once means the system retries until it gets an acknowledgement. The message may arrive more than once. Exactly-once means the downstream effect happens once, even if the transport layer retries.
A third pattern matters in practice: at-most-once. The producer sends once and does not retry. You may lose messages, but you will not get duplicates. Fire-and-forget logging often uses this model.
| Guarantee | Duplicates possible? | Messages lost? | Typical use case | Implementation cost |
|---|---|---|---|---|
| At-most-once | No | Yes | Metrics, non-critical logs | Low |
| At-least-once | Yes | No (with acks) | Order processing, webhooks, email | Medium — needs idempotency |
| Exactly-once effect | No (by design) | No | Payments, ledger entries, inventory | High — app + infra coordination |
The comparison table hides an important detail. Pure exactly-once end-to-end across network boundaries is not achievable in the general case. The two generals problem proves you cannot confirm delivery and absence of duplicates with certainty over an unreliable channel. What engineers call exactly-once is usually effectively exactly-once: at-least-once transport plus idempotent handlers plus deduplication keys.
On a legal-tech portal I built, document upload notifications must not spam clients. The queue uses at-least-once delivery. The notification handler checks a processed-events table before sending email. That is exactly-once effect built on at-least-once transport.
How does at-least-once delivery work in message queues?
At-least-once delivery depends on acknowledgements. The broker holds a message until the consumer confirms processing. If the consumer crashes before acking, or the ack is lost on the network, the broker redelivers.
The acknowledge-and-retry loop
- Producer publishes a message to a queue or topic.
- Broker stores the message durably on disk or replicated storage.
- Consumer receives the message and begins processing.
- Consumer sends an acknowledgement after successful work.
- If step 4 fails or times out, the broker redelivers to another consumer.
Redis, RabbitMQ, Amazon SQS, and Laravel's queue drivers all follow variants of this pattern. Laravel 13.x queue workers call $job->delete() on success. A killed PHP-FPM worker or OOM exit before delete means the job becomes visible again after the visibility timeout.
# Laravel queue worker — at-least-once by default (database driver)
php artisan queue:work database --tries=3 --timeout=90
# .env
QUEUE_CONNECTION=database
DB_CONNECTION=mysql I've seen duplicate order-confirmation emails after a deploy because workers were SIGTERM'd mid-job. The job retried cleanly. The handler was not idempotent. That is a textbook at-least-once failure mode, not a broker bug.
Amazon SQS standard queues guarantee at-least-once delivery. FIFO queues add deduplication within a five-minute window using a deduplication ID. That narrows the duplicate window but does not remove the need for application-level idempotency on longer horizons. See the AWS SQS delivery documentation for visibility timeout behaviour.
For grocery delivery workflows, order status updates ride on at-least-once queues. Inventory decrements use database transactions with row locks. A duplicate job sees zero available stock and exits safely.
Can you achieve true exactly-once delivery in distributed systems?
True exactly-once — one send, one receive, one effect, with no coordination — does not exist across independent services. What vendors sell as exactly-once is transactional coordination between producer, broker, and consumer.
Where broker-level exactly-once appears
Apache Kafka offers idempotent producers and transactional writes when producers, consumers, and broker settings align. The producer assigns a PID and sequence number. The broker deduplicates within a session. Transactions let you write to multiple partitions atomically. This still assumes consumers read within the transaction boundary correctly.
Even Kafka's model has limits. Cross-service exactly-once — payment gateway to your API to your database to an email provider — requires each hop to participate. No single broker spans all four.
Stripe popularised the Idempotency-Key header for payment APIs. Khalti, eSewa, and ConnectIPS callbacks on Nepal projects need the same discipline. Store the gateway transaction ID in a unique index. Return HTTP 200 on duplicates so the gateway stops retrying.
The API rate limiting guide covers abuse prevention. Idempotency complements rate limits. A replay attack and an innocent retry look identical without a dedup key.
How do you implement idempotent consumers for at-least-once delivery?
Idempotency means running the same operation twice produces the same result as running it once. Your handler must tolerate duplicate delivery without creating duplicate side effects.
Pattern 1: Natural idempotency
Some operations are naturally idempotent. Setting a status field to paid twice leaves the row in the same state. Upserting a cache key with the same value is safe. Prefer these where business rules allow.
Pattern 2: Dedup table with unique constraint
Create a processed_events table. Insert the event ID inside the same database transaction as your business write. A duplicate insert hits the unique constraint and rolls back cleanly.
/* migration — MySQL 9.7 / MariaDB 12.3 */
Schema::create('processed_events', function (Blueprint $table) {
$table->string('event_id', 64)->primary();
$table->timestamp('processed_at');
});
/* Laravel 13 job handler */
DB::transaction(function () use ($eventId, $payload) {
ProcessedEvent::create(['event_id' => $eventId]);
Order::where('id', $payload['order_id'])
->update(['status' => 'paid']);
}); Pattern 3: Redis SET NX with TTL
For high-throughput webhook endpoints, Redis SET key 1 NX EX 86400 is fast. If the key exists, return cached response. TTL covers the retry window most gateways use. Redis 8.10 supports this pattern on production clusters I maintain.
Pattern 4: Outbox pattern for cross-service consistency
Write the business event and an outbox row in one transaction. A separate relay process publishes to the queue. Consumers still need idempotency, but you avoid the dual-write problem where the database commits and the queue publish fails.
On client portal projects with document payments, I combine outbox rows with unique gateway reference IDs. The pattern survived Khalti timeout retries without double-charging.
- Generate idempotency keys at the source — never let the consumer invent them.
- Make dedup checks the first step inside the handler, before external API calls.
- Return success to the sender on duplicates so retries stop.
- Log duplicate detections separately from errors for monitoring.
- Use JSON formatting tools to inspect webhook payloads during integration testing.
Which delivery guarantee should Laravel and PHP applications choose?
Default to at-least-once transport with idempotent handlers. Reserve broker-level exactly-once tooling for high-volume Kafka pipelines where your team can operate the complexity. Most Laravel 13.x and Laravel 12.x apps on PHP 8.3+ do not need Kafka transactions.
Laravel queue configuration checklist
# config/queue.php — database driver with retry backoff
'connections' => [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => true,
],
],
# Job class — unique lock via ShouldBeUnique (Redis)
class ProcessPayment implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return $this->paymentId;
}
} Set after_commit => true so jobs dispatch only after the database transaction commits. Without it, a rolled-back order can still enqueue a confirmation email. That is a different class of duplicate — logical, not transport-level.
The Laravel queue documentation documents ShouldBeUnique, failed-job handling, and batch callbacks. For Redis-backed queues, Laravel uses atomic pop operations that approximate at-least-once with short duplicate windows during failover.
Match infrastructure to guarantee needs:
- Email and SMS notifications: at-least-once plus dedup table — duplicate email is annoying, not catastrophic if caught.
- Payment capture: effectively exactly-once via gateway idempotency keys and unique DB constraints.
- Inventory decrement: transactional row lock or atomic
UPDATE stock SET qty = qty - 1 WHERE qty > 0. - Analytics events: at-most-once or at-least-once with downstream dedup in the warehouse.
- Audit logs: append-only with event UUID — duplicates visible but harmless if keyed.
For enterprise application builds, document the delivery guarantee per integration in your API spec. Payment partners expect idempotent POST endpoints. Logistics APIs often tolerate at-least-once status pings.
Deploy practices matter too. Zero-downtime releases with Deployer 7 send SIGTERM to queue workers. Configure stopwaitsecs high enough for in-flight jobs to finish. I use the same GitLab CI pipeline on sister legal-tech sites — worker graceful shutdown prevents unnecessary redelivery storms after every deploy.
Monitoring closes the loop. Track duplicate-detection counts, queue depth, and failed-job rates in Horizon or a Prometheus stack. A spike in dedup hits after a gateway outage is healthy. Zero dedup hits during known retries means your handler is probably not idempotent.
Read advanced Eloquent patterns for transaction boundaries that pair with queue dispatch. Review progressive delivery when rolling out idempotency changes — feature flags let you shadow-test dedup logic before cutover.
On WooCommerce 11.1 stores like florist projects, Action Scheduler retries failed hooks at least once. Custom payment plugins must follow the same idempotency rules as Laravel jobs. WordPress does not give you exactly-once for free.
Cost matters for Nepal clients. Exactly-once infrastructure — Kafka clusters, distributed transactions, multi-AZ FIFO queues — adds Rs 15,000–40,000/month (~USD 110–295) in cloud spend. At-least-once on a single Redis or database queue with careful application code costs far less and fails more predictably for small teams.
Key Takeaways
- At-least-once is the practical default for queues, webhooks, and Laravel workers — plan for duplicates from day one.
- True exactly-once across services is impossible; achieve exactly-once effects with idempotency keys and unique database constraints.
- Check dedup before any external side effect — payment API call, email send, inventory decrement.
- Return HTTP 200 on duplicate webhook deliveries so upstream retries stop.
- Set
after_commit => trueon Laravel queues to prevent phantom jobs from rolled-back transactions. - Monitor duplicate-detection metrics — silence during retry storms means your handler has a gap.
People Also Ask
Is exactly-once delivery actually possible?
Not end-to-end across independent network services in the general case. Brokers like Kafka offer exactly-once semantics within their ecosystem. Application teams achieve the same outcome by combining at-least-once delivery with idempotent consumers and deduplication stores.
What happens if I ignore duplicate messages?
Duplicates cause double charges, repeated booking confirmations, inflated inventory deductions, and duplicate SMS costs. On payment integrations I've maintained, gateway retries within 24 hours are common after network blips. Handlers must tolerate them.
How is idempotency different from deduplication?
Deduplication detects and skips duplicate messages using a stored key. Idempotency means the operation itself is safe to repeat — setting status to shipped twice has the same effect as once. Production systems use both: dedup at the entry point, idempotent logic inside the handler.
Does Laravel support exactly-once queue delivery?
Laravel queues provide at-least-once delivery by default. Use ShouldBeUnique, database unique constraints, and after_commit dispatch to achieve exactly-once effects. For stricter guarantees, integrate a broker with native deduplication and design consumers accordingly.
Ship integrations that survive retries
Exactly-Once vs At-Least-Once Delivery is not an abstract distributed-systems debate. It determines whether your payment gateway integration double-charges, whether your booking system sends two confirmation emails, and whether your queue workers survive deploys without corrupting data. Default to at-least-once transport, enforce exactly-once effects in application code, and document the guarantee per integration.
If you are building payment flows, webhooks, or queue-heavy Laravel systems and want the architecture reviewed before production traffic hits, contact us for an integration audit. For ongoing queue monitoring and deploy hardening, see support and maintenance services or explore related work in the booking platform portfolio.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

