{"slug": "laravel-waiting-request", "title": "Laravel Waiting Request", "summary": "The article describes a common concurrency problem in web applications where a background job processes data while a subsequent request attempts to read that same data, potentially returning stale or incorrect information. It introduces the `aihimel/laravel-waiting-request` Laravel package, which solves this by allowing a request to \"park\" and wait until the background job signals that the resource is ready, using a cache-backed system of blockers and resolvers. The package provides a simple API with methods like `addBlocker`, `whenResolved`, and `resolveBlocker`, and includes safety features such as automatic expiry for crashed jobs to prevent indefinite waiting.", "body_md": "## The Problem\n\nYou are processing some data through background job. But before the processing is done, another request had been made to read the related data.\n\nIn this case you are either providing a historic data or serving wrong information.\n\n## Solution\n\nHolding the request until the job is executed, could be the simplest solution.\n\nI am not saying it is the only solution, but the simplest one.\n\n## Some Scenarios\n\nLets discuss about some possible scenarios.\n\n### Booking Job\n\nWhen a user request to book a resource between two specifics dates. Let's assume that it is done by a job. So it might take some time in production load.\n\nIn the meantime if another request is asking for that specific users booking data.\n\n### File Importing Job\n\nUser uploads a file like CSV or XML, you have accepted the file but it also needs processing, which should be done in by a job.\n\nIf the user ask for the status of the CSV resource in another request.\n\n## The Package\n\n[ aihimel/laravel-waiting-request](https://packagist.org/packages/aihimel/laravel-waiting-request) is a small Laravel package that solves exactly this — it lets one request\n\n*park*until another piece of work (a job, a sync, a long-running controller action) signals that the resource is ready to read.\n\n### Install\n\n```\ncomposer require aihimel/laravel-waiting-request\n```\n\nOptionally publish the config:\n\n```\nphp artisan vendor:publish --tag=\"waiting-request-config\"\n```\n\n### How it works\n\nThe package exposes a tiny API around four ideas: **block**, **wait**, **check**, **resolve**. Under the hood it is backed by your Laravel cache — no extra infrastructure, no queue plumbing.\n\nA blocker is identified by a *class path* and a *resource id*. That pair becomes a unique cache key, so blockers are per-resource (booking `42`\n\ndoes not interfere with booking `43`\n\n).\n\n``` php\nuse Aihimel\\LaravelWaitingRequest\\Facades\\LWRequest;\n\n// 1. Block — call this where the background work *starts*\nLWRequest::addBlocker(Booking::class, $booking->id);\n\n// 2. Wait — call this in the request that wants to read the resource\n$resolved = LWRequest::whenResolved(Booking::class, $booking->id);\n\nif ($resolved) {\n    return BookingResource::make($booking->fresh());\n}\n\nreturn response()->json(['message' => 'Still processing, try again'], 202);\n\n// 3. Resolve — call this when the background work finishes\nLWRequest::resolveBlocker(Booking::class, $booking->id);\n```\n\nYou can also peek without waiting:\n\n``` php\nif (LWRequest::isBlocked(Booking::class, $booking->id)) {\n    // resource is mid-flight\n}\n```\n\n### Applying it to the scenarios\n\n**Booking job.** The controller that accepts the booking calls `addBlocker(Booking::class, $id)`\n\nand dispatches the job. The job calls `resolveBlocker(...)`\n\nin its `handle()`\n\n(or in a `finally`\n\nblock). Any reader that hits `GET /bookings/{id}`\n\nin the meantime calls `whenResolved(...)`\n\nfirst and only reads the model once the writer is done.\n\n**File importing job.** Same shape: `addBlocker(Import::class, $import->id)`\n\nwhen the upload is accepted, `resolveBlocker(...)`\n\nwhen the parser finishes (success *or* failure — both should release). The status endpoint calls `whenResolved(...)`\n\nso the client gets a settled answer instead of a half-imported snapshot.\n\n### Sensible defaults you can tune\n\nEvery knob lives in `config/waiting-request.php`\n\nand is overridable via env:\n\n| Config | Env | Default | What it does |\n|---|---|---|---|\n`cache_prefix` |\n`LW_REQUEST_CACHE_PREFIX` |\n`lw_request_` |\nNamespace for cache keys |\n`timeout` |\n`LW_REQUEST_MAX_WAITING_TIME` |\n`5` |\nHow long `whenResolved()` waits before giving up (seconds) |\n`check_interval` |\n`LW_REQUEST_CHECK_INTERVAL` |\n`250` |\nPoll interval inside `whenResolved()` (milliseconds) |\n`max_blocking_time` |\n`LW_REQUEST_MAX_BLOCKING_TIME` |\n`10` |\nMax lifetime of a blocker before it auto-expires (seconds) |\n\n`addBlocker()`\n\ntakes an optional third argument so you can bump the TTL per call when you know a particular job runs longer:\n\n``` php\nLWRequest::addBlocker(Import::class, $import->id, 120); // 2 minutes\n```\n\n### Why the blocker has a lifetime\n\nIf a job crashes before calling `resolveBlocker()`\n\n, you do **not** want readers to wait forever. From v2.x every blocker carries a Unix expiry timestamp. The next `isBlocked()`\n\n/ `whenResolved()`\n\ncall after that timestamp will:\n\n- Forget the cache entry, and\n- Emit\n`Log::warning('Waiting-request blocker expired without being resolved', [...])`\n\nSo even if your job dies, traffic recovers on its own and you get a log line telling you it happened.\n\n## Do's and Don'ts\n\n### Do\n\n-\n**Do release the blocker in** Wrap your job body so a thrown exception still hits`finally`\n\n.`resolveBlocker()`\n\n. Auto-expiry is a backstop, not a happy path. -\n**Do set** If your import averages 8s and worst-cases at 25s, a 10s default will auto-release while the job is still running — defeating the lock.`max_blocking_time`\n\nto comfortably exceed your worst-case job duration. -\n**Do tune** If a client is willing to wait 2s for a synchronous response, set`timeout`\n\nto match your UX budget.`timeout=2`\n\n; do not let`whenResolved()`\n\nhold an HTTP worker for 30s. -\n**Do flush the cache when upgrading from 1.x to 2.x.** Pre-upgrade values stored as`true`\n\nwill be read as`1`\n\n, treated as already-expired, and produce a one-time burst of warning logs. -\n**Do treat a** Respond with`false`\n\nreturn from`whenResolved()`\n\nas \"still pending\".`202 Accepted`\n\n(or similar) and let the client poll — do not pretend the data is ready.\n\n### Don't\n\n-\n**Don't put** It evicts expired entries and writes a log line. That is intentional, but worth knowing.`isBlocked()`\n\non a hot, read-only path you expect to be side-effect-free. -\n**Don't use it as a distributed mutex for** This package is for*writes*.*readers waiting on writers*on a best-effort basis. If two writers race,`Cache::add()`\n\nwill reject the second`addBlocker()`\n\n(it returns`false`\n\n), but the package does not give you queueing, fairness, or strict mutual exclusion. -\n**Don't share a single blocker across unrelated resources.** Key it by the real resource (`Booking::class + $id`\n\n), not by something coarse like the user id, or you will block requests that have nothing to do with each other. -\n**Don't forget the cache driver matters.**`array`\n\nor`file`\n\ndrivers will not work across processes. In production, use`redis`\n\n/`memcached`\n\nso the worker that resolves the blocker and the web process that is waiting actually share the same cache. -\n**Don't lean on** Polling inside a worker burns a worker slot. Workers should`whenResolved()`\n\nfrom a queue worker.*resolve*blockers, not*wait*on them.\n\nThat's the whole package — a couple of facade calls, a cache key per resource, and a sane expiry so nothing wedges. If you've ever shipped a `?retry=true`\n\nhack or a sleep-and-pray in a controller, this is the cleaner version of that.\n\nSource & issues: [github.com/aihimel/laravel-waiting-request](https://github.com/aihimel/laravel-waiting-request)", "url": "https://wpnews.pro/news/laravel-waiting-request", "canonical_source": "https://dev.to/aihimel/laravel-waiting-request-27o4", "published_at": "2026-05-23 06:50:00+00:00", "updated_at": "2026-05-23 07:01:38.709420+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["Laravel", "aihimel/laravel-waiting-request"], "alternates": {"html": "https://wpnews.pro/news/laravel-waiting-request", "markdown": "https://wpnews.pro/news/laravel-waiting-request.md", "text": "https://wpnews.pro/news/laravel-waiting-request.txt", "jsonld": "https://wpnews.pro/news/laravel-waiting-request.jsonld"}}