Lukas Weber 00:00
Welcome. Today we’re talking about building PHP background jobs that stay dependable after the first busy day in production. The focus is practical: what goes on a queue, how retries behave, how duplicate delivery stays harmless, and what operators need to see.
Lukas Weber 00:16
The central idea is simple but demanding: reliability is designed into the system. It comes from making delivery guarantees, failure paths, security boundaries, and operational signals explicit before an incident forces those decisions.
Lukas Weber 00:30
Start with the request path. A web request should usually confirm the user’s immediate action quickly. It is a poor place to wait for a slow email provider, resize a large upload, or synchronize data with a service that may be unavailable.
Lukas Weber 00:45
Picture a customer submitting an order. The application should validate it, store it, and return a useful confirmation. Sending the receipt, generating an invoice, and notifying a warehouse can become separate jobs with their own capacity and failure handling.
Lukas Weber 01:00
That separation improves response time, but it changes the contract. Completion is now eventual. A user may see an order confirmed before a receipt arrives, so the product experience and support tooling need to acknowledge that distinction.
Lukas Weber 01:14
A useful test is to ask three questions: Is this work slow? Can an external dependency fail independently? Does it need to scale differently from HTTP traffic? A yes to any of them is a strong signal for a job boundary.
Lukas Weber 01:29
Don’t queue everything by reflex. A tiny database update needed to render the response belongs in the request. Queues add latency, serialization, monitoring, and distributed failure modes. Use them where those costs buy meaningful isolation.
Lukas Weber 01:44
Once work is queued, assume at-least-once delivery. A worker can finish an action, lose its connection before acknowledging the message, and receive that message again. That is normal behavior, not proof that the queue is broken.
Lukas Weber 01:57
So what should retry? A timeout reaching an email provider may be transient. A temporary database connection failure may be transient. An invalid recipient address, malformed payload, or deleted customer record is usually permanent until somebody changes the underlying data.
Lukas Weber 02:14
Treating every exception as retryable creates a retry storm. Hundreds of workers can repeatedly hit the same unavailable provider, consume capacity, and obscure the original problem. Classify errors close to the job’s domain, not merely by exception type.
Lukas Weber 02:30
Use bounded retries with backoff. For example, retry a transient provider failure after a short delay, then progressively longer delays, and stop after a defined number of attempts. Add jitter so many failed jobs do not return simultaneously.
Lukas Weber 02:46
Set a timeout for the job itself, too. If an image conversion normally takes thirty seconds, a worker that runs for twenty minutes is probably stuck. The timeout must sit comfortably below the queue’s visibility window, or another worker may start duplicate work early.
Lukas Weber 03:02
After the retry limit, move the message to a reviewable failure path, often called a dead-letter queue. Preserve the job identifier, safe error context, attempt count, and payload reference. The goal is diagnosis and deliberate recovery, not silent disappearance.
Lukas Weber 03:18
Alerts need restraint. Page someone when failed jobs are growing quickly, a critical queue is aging beyond its service objective, or workers have stopped consuming. A single ordinary retry is a signal for a dashboard, not necessarily a midnight escalation.
Lukas Weber 03:34
Retries lead directly to idempotency: executing the same job twice should produce the intended outcome once. This matters most when a job creates an external side effect, such as charging a card, sending a message, or provisioning an account.
Lukas Weber 03:48
Give the work a stable idempotency key derived from the business event, not the individual attempt. For an invoice receipt, that might be the invoice ID plus the receipt type. Every retry carries the same key.
Lukas Weber 04:01
Inside your database, enforce that intention with a uniqueness constraint where possible. A table recording a notification or a processed event can reject a second insert. Application-level checks alone are vulnerable when two workers race each other.
Lukas Weber 04:16
For payments or messaging providers, pass the same idempotency key if the provider supports it. Then a retry after an uncertain network response can ask the provider to recognize the original operation rather than create a second charge or message.
Lukas Weber 04:30
There is one awkward boundary: writing to your database and publishing a queue message are separate systems. A transactional outbox can help. Store the business change and an outbound event in one database transaction, then publish that event reliably afterward.
Lukas Weber 04:46
That pattern does not make the system magically exactly-once. It makes the remaining duplicates manageable. The outbox publisher may publish more than once, but consumers that use stable keys and constraints can safely absorb those repeats.
Lukas Weber 05:00
Reliable systems are visible systems. Give every job a job ID and carry a correlation ID from the originating request or business event. That lets a support engineer trace one customer action through an HTTP request, queued job, provider call, and final state.
Lukas Weber 05:17
Track clear state transitions: queued, running, succeeded, retrying, and failed. Measure queue age, processing latency, throughput, retry counts, failure rate, and worker saturation. A dashboard should answer whether the system is catching up or falling behind.
Lukas Weber 05:34
Use structured logs with useful context: job type, IDs that are safe to expose internally, attempt number, failure classification, and dependency name. Avoid dumping the full payload. Logs are searchable evidence, but they can also become a data leak.
Lukas Weber 05:50
Workers deserve least-privilege credentials. An email worker should not have permission to alter billing tables. Separate credentials by role, rotate secrets, and keep sensitive payload fields encrypted or referenced securely when the queue infrastructure requires it.
Lukas Weber 06:06
Validate untrusted inputs before they become jobs, and validate assumptions again in the worker. Queueing data does not make it trustworthy. Check authorization-sensitive fields, constrain file locations, and never let a payload choose arbitrary commands, classes, or network destinations.
Lukas Weber 06:23
Here is a practical production checklist. Define a small job contract with a versioned payload, stable business identifier, expected timeout, and explicit success condition. Decide whether each failure is retryable, permanent, or needs manual review.
Lukas Weber 06:39
Set concurrency limits per job class and per downstream dependency. A hundred image workers may be fine; a hundred simultaneous calls to a rate-limited payment API are not. Capacity controls are part of correctness, not merely performance tuning.
Lukas Weber 06:55
Test the uncomfortable paths before launch: worker termination during work, provider outages, duplicate messages, expired visibility windows, malformed payloads, and poisoned jobs. Launch with dashboards and alerts already connected, then tune thresholds from real traffic.
Lukas Weber 07:12
The takeaway is this: safe retries require idempotent job design, and idempotency needs concrete support from stable keys, database constraints, and provider-aware side effects. Combine that with bounded retries and useful visibility, and failures become operable rather than mysterious.
Lukas Weber 07:30
Thanks for listening. Keep the request path focused, make repeated work safe, and give your team enough evidence to act when dependencies misbehave. Take care.