- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
At-Least-Once Delivery: Why Your Queue Will Run Some Jobs Twice
About Post
A worker picks up a job: "send the payment receipt". It sends the email. Then, a split second before it can tell the queue "done", the server restarts for a deploy.
The queue never heard back. As far as it knows, the job was lost. So, doing exactly what it promised, it hands the job to another worker. The tenant gets two receipts.
Nothing was broken here. No bug, no misconfiguration. This is the queue keeping its promise, and the promise is called at-least-once delivery. Once you understand it, a whole class of "how did this run twice?" mysteries stop being mysteries.
The three promises a queue can make
Every messaging system has to pick what it guarantees when things go wrong:
- At most once: a message is delivered zero or one times. If something fails, it's lost. Fine for things like metrics where a missing data point doesn't matter.
- At least once: a message is delivered one or more times. Nothing gets lost, but duplicates are possible. This is the default for most job queues: Laravel's queue drivers, SQS standard queues, RabbitMQ with acknowledgements and many others.
- Exactly once: one delivery, always. This is what everyone wants. Hold that thought.
Where the duplicates come from
The core problem is that "do the work" and "tell the queue it's done" are two separate steps, and the world can end between them. A worker processes a message in roughly this order:
- Take the message, which becomes invisible to other workers for a while.
- Do the work: write to the database, call an API, send an email.
- Acknowledge (delete) the message.
If anything fails between step 2 and step 3, the work happened but the acknowledgement didn't. The message becomes visible again and someone runs it a second time. Common causes:
- A worker crashes, gets killed by a deploy, or runs out of memory.
- The network drops between the worker and the queue.
- A job takes longer than the "invisible" window, so the queue assumes it died and hands it to another worker while the first one is still running.
That last one deserves its own section, because in Laravel it's one of the most common sources of duplicates.
The Laravel gotcha: retry_after vs timeout
In config/queue.php, each connection has a retry_after value: how many seconds the queue waits for a job before assuming it failed and releasing it again. Separately, the worker has a --timeout (or a job's $timeout): how long a job may run before the worker kills it.
If a job can run for 120 seconds but retry_after is 90, then at second 90 the queue hands the job to another worker. Now two copies run at the same time. The Laravel docs warn about exactly this: --timeout should always be several seconds shorter than retry_after, so a stuck job is killed before it's released.
// config/queue.php
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 190,
],
// worker: php artisan queue:work --timeout=180
Getting this right removes the "two at once" duplicates. It does nothing about the crash-before-acknowledge duplicates. For those, there's only one real answer.
Myth: "just use exactly-once delivery"
Exactly-once delivery across a network, to a consumer that has side effects in the outside world, isn't something a queue can promise on its own. The worker can always fail after doing the work but before reporting it. No protocol can make "sent the email" and "told the queue" happen as one atomic step, because they're different systems.
When you see "exactly-once" in a product's docs, read the small print. It usually means one of these:
- Deduplication within a window. SQS FIFO queues, for example, drop duplicate messages sent with the same deduplication ID within a few minutes. That protects against duplicate sends, not against your worker running a message twice after a crash.
- Exactly-once inside one system. Kafka's transactions can make reading and writing within Kafka atomic. The moment your consumer charges a card or sends an SMS, you're outside that boundary.
The honest formula: exactly-once effect = at-least-once delivery + an idempotent consumer. The queue makes sure nothing is lost. Your code makes sure duplicates don't matter.
Writing a consumer that shrugs off duplicates
An idempotent consumer can process the same message twice and leave the world exactly as if it ran once. The most reliable way is to give every message a stable ID and record which ones you've handled, in the same transaction as the work itself:
public function handle(): void
{
DB::transaction(function () {
$isNew = DB::table('processed_messages')->insertOrIgnore([
'message_id' => $this->messageId, // unique column
'processed_at' => now(),
]);
if ($isNew === 0) {
return; // seen it already, nothing to do
}
Payment::where('reference', $this->reference)
->update(['status' => 'confirmed']);
});
}
The unique index on message_id does the heavy lifting. If two workers race, only one insert succeeds. And because the marker and the work commit together, a crash rolls back both, so the retry starts clean.
The ID has to come from the message, not be generated in the job. For webhooks, the provider's event ID is perfect. For your own jobs, create an ID when you dispatch and pass it in.
When the side effect isn't in your database
The transaction trick only covers your own database. An email, an SMS or a call to a payment gateway can't be rolled back. A few practical options:
- Pass an idempotency key downstream. Many payment APIs accept one, so a retried call doesn't charge twice.
- Record "sent" right after sending and check it before sending. There's still a tiny window, but it shrinks the problem from "every crash" to "a crash in the exact millisecond between two lines".
- Make duplicates harmless. A second identical receipt is annoying. A second charge is a support ticket. Spend your effort where the duplicate actually hurts.
One more trap: Laravel's ShouldBeUnique stops the same job from being queued twice while one is pending. It's useful, but it doesn't stop a job from being run twice after a crash. It's not a replacement for idempotency.
The checklist
- Assume every job and every webhook will occasionally run twice.
- Set the worker timeout shorter than
retry_after. - Give messages stable IDs and record processed IDs behind a unique index.
- Do the work and the "processed" marker in one transaction where you can.
- For external side effects, use idempotency keys or make duplicates harmless.
What's the most memorable duplicate you've seen from a queue: a double email, a double charge, or something stranger?

Be first to comment it...