Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Use a Laravel queue to fetch and store a remote PDF, then let a separate authorized HTTP request download the stored file. A queue job cannot keep the original browser response open or deliver bytes directly; it moves the slow retrieval out of that request. The reliable flow is: create a download record, dispatch a job with stable identifiers, save the completed file to a configured filesystem disk, mark the record ready, and return it through a protected download endpoint.

This guide targets Laravel 13.x documentation reviewed on September 29, 2026, and covers downloading an existing remote PDF. Generating a new PDF is a different workflow.

How the queued PDF download works

  1. Create a request record. Store the authenticated owner, source URL, status, disk and eventual path.
  2. Dispatch a job. Pass the record ID and source information, not the PDF contents.
  3. Fetch in a worker. Apply a timeout, inspect the upstream response, validate the payload for your source, and write it to a private disk.
  4. Mark completion. Update status only after storage succeeds.
  5. Download later. An authorized endpoint uses the stored disk and path to return the file.

Laravel’s queue abstraction supports database, Amazon SQS, Redis, Beanstalkd, synchronous and null drivers; a connection identifies the backend and a connection can contain named queues. Laravel’s filesystem abstraction similarly lets a configured disk use local, SFTP or S3 storage. See the queue documentation and filesystem documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Configure a queue and private storage

Local development

Set QUEUE_CONNECTION=database (or another driver) in .env, create the queue tables, and run migrations:

php artisan make:queue-table
php artisan migrate
php artisan queue:work database --queue=pdf-downloads

Use a dedicated queue name when PDF retrieval should have its own worker capacity. The sync driver is useful for debugging because it runs immediately, but it does not test background execution.

Production choices

Option When it fits Trade-off
Database Already operating a relational database and modest throughput Simple deployment, but jobs compete for database resources
Redis Redis is already monitored and workers need fast queue operations Requires Redis operations and capacity planning
Amazon SQS Managed queue infrastructure is preferred Visibility and retry behavior must be aligned with worker settings
Beanstalkd Existing Beanstalkd environment Another service to operate

There is no universally best backend. Select one your team can monitor, secure and recover.

Filesystem disk

Configure a private disk in config/filesystems.php (for example, local or S3), and keep credentials in environment variables. Do not put confidential PDFs on a public disk merely to simplify downloads.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create a download record

A minimal table should include user_id, source_url, disk, path, status, failure_reason, timestamps and, optionally, response metadata. Use statuses such as queued, processing, ready and failed. The record is the stable hand-off between the web request, worker and later download.

Dispatch from a controller

use AppJobsFetchRemotePdf;
use AppModelsPdfDownload;
use IlluminateHttpRequest;

public function store(Request $request)
{
    $data = $request->validate([
        'url' => ['required', 'url', 'max:2048'],
    ]);

    $download = PdfDownload::create([
        'user_id' => $request->user()->id,
        'source_url' => $data['url'],
        'disk' => 'local',
        'status' => 'queued',
    ]);

    FetchRemotePdf::dispatch($download->id)->onQueue('pdf-downloads');

    return response()->json([
        'id' => $download->id,
        'status' => $download->status,
    ], 202);
}

Pass an ID rather than serializing a model with large or mutable data. In production, restrict outbound URLs to the sources your application is allowed to contact; otherwise a downloader can become a server-side request forgery risk.

Implement the queued fetch job

The example below uses Laravel’s HTTP client facade and filesystem API. Confirm exact HTTP-client behavior and options against the Laravel release installed in your application, especially if you need streaming for very large files.

namespace AppJobs;

use AppModelsPdfDownload;
use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateHttpClientRequestException;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;
use IlluminateSupportFacadesHttp;
use IlluminateSupportFacadesStorage;
use Throwable;

class FetchRemotePdf implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;
    public $backoff = [30, 120, 300];
    public $timeout = 300;

    public function __construct(public int $downloadId) {}

    public function handle(): void
    {
        $download = PdfDownload::findOrFail($this->downloadId);
        $download->update(['status' => 'processing']);

        $response = Http::timeout(120)->get($download->source_url);
        $response->throw();

        $body = $response->body();
        if ($body === '') {
            throw new RuntimeException('The upstream response was empty.');
        }

        // Validate according to your source. This basic check is not a substitute
        // for MIME, signature, size and malware policy appropriate to your system.
        if (! str_starts_with($body, '%PDF-')) {
            throw new RuntimeException('The upstream body is not recognized as a PDF.');
        }

        $path = 'pdf-downloads/'.$download->id.'/document.pdf';
        if (! Storage::disk($download->disk)->put($path, $body)) {
            throw new RuntimeException('The PDF could not be stored.');
        }

        $download->update([
            'path' => $path,
            'status' => 'ready',
            'failure_reason' => null,
        ]);
    }

    public function failed(Throwable $e): void
    {
        PdfDownload::whereKey($this->downloadId)->update([
            'status' => 'failed',
            'failure_reason' => $e->getMessage(),
        ]);
    }
}

The PDF signature check is intentionally conservative: some valid delivery systems transform or wrap content, while a malicious response can begin with a PDF signature. Add content-length limits, MIME checks, malware scanning, redirect and authentication rules appropriate to your upstream. For large files, use a memory-conscious streaming design supported by your installed HTTP client and storage adapter rather than buffering the whole body; verify those APIs for your version.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make retries safe

Retries can run after a timeout even when an upstream request partly succeeded. Write to an ID-specific path, overwrite only after a complete write, and update ready last. Consider deleting a partial object on failure and making the job idempotent when a ready record already has a verified file.

Return the completed PDF safely

use AppModelsPdfDownload;
use IlluminateHttpRequest;
use IlluminateSupportFacadesStorage;

public function download(Request $request, PdfDownload $download)
{
    abort_unless($download->user_id === $request->user()->id, 404);
    abort_unless($download->status === 'ready' && $download->path, 404);

    abort_unless(Storage::disk($download->disk)->exists($download->path), 404);

    return Storage::disk($download->disk)->download(
        $download->path,
        'document-'.$download->id.'.pdf',
        ['Content-Type' => 'application/pdf']
    );
}

Laravel documents that Storage::download(path, filename, headers) generates a response that forces the browser to download the file. Authorize before revealing whether a path exists, and use a private disk for private documents. Where supported by the configured disk, temporaryUrl can provide an expiring link instead of proxying bytes through Laravel; set an expiry that matches your threat model and product flow.

Status endpoint and UI

Expose only the owner’s record and return queued, processing, ready or failed. Poll or use your existing notification system. Show the download link only for ready; show a retry action or support path for failed. Never treat dispatch success as file completion.

Worker lifecycle, timeouts and cleanup

Run a supervised worker in production and monitor failed jobs. Set job timeout, attempts and backoff from measured retrieval and storage time, upstream limits and worker resources. A job timeout should not be equal to or greater than the queue connection’s retry or visibility interval without understanding duplicate execution risk. Laravel documents worker timeouts, attempts, retries and failed-job handling, but no universal numeric value fits every PDF.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Schedule cleanup for expired records and stored objects, and log the upstream host, status, elapsed time and resulting path without logging secrets or sensitive PDF contents. Keep retention, encryption, access logs and deletion requirements explicit.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting

The request returns 202 but no file appears

A 202 means the job was accepted. Confirm a worker is running with the same environment, connection and queue name: php artisan queue:work database --queue=pdf-downloads. Inspect failed jobs and worker logs.

The job fails with a timeout

Measure DNS, connection, download and storage time separately. Check upstream limits and worker CPU/memory, then adjust timeout and visibility settings together. Do not simply make retries infinite.

The downloaded file is HTML

Many login pages, bot checks and error pages return HTTP 200. Inspect status, headers, final URL and the body signature; require authentication headers or cookies when legitimately needed, and reject unexpected content.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Users can access another user’s PDF

Enforce ownership or policy authorization before checking status or calling download. Do not expose raw object keys or public URLs for private documents.

Duplicate files appear after retries

Use deterministic paths keyed by the download ID, make writes idempotent, and update the database only after the final object exists. Investigate queue visibility and worker timeout alignment.

Memory usage is excessive

$response->body() buffers the payload. For large PDFs, implement documented streaming APIs for your exact Laravel and HTTP-client versions, enforce a maximum size, and test interruption and cleanup behavior.

Or skip the browser setup:

If your queued workflow needs screenshots or PDFs of web pages rather than an existing PDF URL, ScreenshotNeo provides a one-call API. It removes cookie/consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads and cache hits are not billed, and response headers identify the page verdict and billing. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for options such as PDF paper size, margins, page ranges, waits, headers, cookies, selectors and webhooks. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can a queued job return the PDF directly?

No. The browser request and worker execution are separate. Return a request ID, then download after the record is ready.

Is this the same as queued PDF generation?

No. This pattern retrieves an existing remote PDF. Packages that queue PDF generation address rendering and have different APIs and constraints.

Should I use a temporary URL?

Use one when your configured disk supports it and an expiring, direct object link fits your authorization model; otherwise stream through an authorized controller.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.