October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
Laravel

How to Download Remote PDFs with Laravel Queues (Laravel 13)

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

Use a queued job to fetch and store a remote PDF, then let a separate authorized web request deliver the completed file. A queue moves slow network work out of the request; it does not keep the original browser request open or download the file to the user by itself. This guide targets Laravel 13.x and focuses on retrieving an existing PDF, not generating one.

How the queued PDF download should work

Separate retrieval from delivery. The web request records the requested document and dispatches a job. A worker fetches the remote file, validates the response, saves it to a configured Laravel filesystem disk, and marks the record ready. A later request checks the user’s permission and returns the stored file.

  1. Create a download record with the requesting user, source URL or source identifier, status, and eventual disk and path.
  2. Dispatch a job with the record ID and only the stable information it needs. Avoid serializing large file contents into the job.
  3. Have a running worker fetch the source, handle expected failures, and persist the PDF.
  4. Mark the record ready only after the write succeeds.
  5. Expose a status endpoint or page; when ready, an authorized route returns a download response or an appropriate temporary URL.

Laravel describes queues as a way to defer time-intensive tasks and respond to web requests without waiting for those tasks to finish. The queue and filesystem are documented as separate facilities, so the end-to-end flow is an application design built from both. Laravel 13.x queue documentation · Laravel filesystem documentation.

Choose the queue and storage setup

Queue connection versus queue name

A connection selects the backend service; a queue name selects a lane within that connection. Laravel 13.x documents database, Amazon SQS, Redis, Beanstalkd, synchronous, and null drivers. Pick based on infrastructure you already operate, worker deployment, visibility needs, and expected workload—not on a universal ranking. A dedicated queue name can let you allocate workers or priority separately for PDF retrieval.

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

For local debugging, a synchronous connection can execute the job inline. That is useful for checking application behavior but does not demonstrate that a production worker is running asynchronously. In production, deploy and supervise workers for the chosen connection and queue.

Select a disk and privacy model

Laravel disks abstract the storage driver and location. The 13.x filesystem documentation describes local, SFTP, and S3 drivers. Choose a private disk for documents that should not be publicly reachable, and store both the disk name and relative path with the download record. Retention and cleanup are application responsibilities: decide how long completed PDFs should remain and remove expired files and records consistently.

Use either an authorized application route that calls Storage::download or a temporary URL if the configured disk supports it and its expiry/access semantics fit the product. Laravel documents temporary URLs for local and S3 storage under the required configuration. A URL is not a substitute for checking document access policy.

Implement the request, job, and download route

The following is an architectural Laravel 13 example rather than a tested drop-in application. It assumes a PdfDownload model with user_id, source_url, status, disk, path, and original_name fields, plus a policy that authorizes viewing a record. Configure the HTTP client and queue behavior for your framework release and upstream service; Laravel’s cited queue and filesystem pages do not define a complete remote-fetch validation recipe.

Free tools Windows power users keep installed

One-click scans. No signup required.

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

Accept the request and dispatch

<?php

namespace AppHttpControllers;

use AppJobsFetchPdf;
use AppModelsPdfDownload;
use IlluminateHttpRequest;

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

        // Also enforce an application-specific allowlist or SSRF policy.
        $download = PdfDownload::create([
            'user_id' => $request->user()->id,
            'source_url' => $data['url'],
            'status' => 'queued',
        ]);

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

        return response()->json([
            'id' => $download->id,
            'status' => $download->status,
            'status_url' => route('pdf-downloads.show', $download),
        ], 202);
    }
}

Returning 202 Accepted and a status URL tells the client that work was accepted, not that the document is already downloadable. Validate and constrain source URLs: allowing arbitrary user-supplied URLs can expose internal network services or other resources, so apply an explicit host/scheme policy appropriate to your application.

Fetch, validate, and store in the job

Use the HTTP client API documented for the exact Laravel version in your application. The essential behavior below is illustrative: set a finite timeout, check status, reject a response that is not plausibly a PDF, persist it under a deterministic or uniquely managed path, and change status only after storage succeeds. Confirm the response streaming and storage APIs against your installed version, especially for large files.

<?php

namespace AppJobs;

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

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

    public int $tries = 3;
    public int $timeout = 120;

    public function __construct(public int $downloadId) {}

    public function backoff(): array
    {
        return [10, 30];
    }

    public function handle(): void
    {
        $download = PdfDownload::findOrFail($this->downloadId);

        // Idempotency: a completed record should not be fetched again on redelivery.
        if ($download->status === 'ready') {
            return;
        }

        $download->update(['status' => 'processing']);

        $response = Http::timeout(90)
            ->withOptions(['allow_redirects' => true])
            ->get($download->source_url);

        $response->throw();
        $contentType = strtolower($response->header('Content-Type', ''));
        $body = $response->body();

        if (! str_contains($contentType, 'pdf') && ! str_starts_with($body, '%PDF-')) {
            throw new RuntimeException('The upstream response does not appear to be a PDF.');
        }

        // For large files, use a version-appropriate streaming approach rather than
        // buffering the entire response body in memory.
        $disk = 'local';
        $path = 'private/pdfs/'.$download->id.'.pdf';
        Storage::disk($disk)->put($path, $body);

        $download->update([
            'disk' => $disk,
            'path' => $path,
            'original_name' => 'document-'.$download->id.'.pdf',
            'status' => 'ready',
        ]);
    }

    public function failed(?Throwable $exception): void
    {
        PdfDownload::whereKey($this->downloadId)->update(['status' => 'failed']);
        // Log a safe diagnostic; avoid logging credentials or sensitive URLs.
    }
}

The sample deliberately does not claim that a content-type header proves the body is valid: upstream servers can mislabel responses, return HTML error pages with a successful status, or send malformed files. The signature check is only a basic screening step, not comprehensive PDF validation. Add a maximum size, allowed hosts, redirect policy, authentication handling, and stronger validation where the source and risk require them. The buffered body() example is unsuitable for arbitrarily large PDFs; select a streaming design supported by your HTTP client and storage adapter.

Ensure your schema permits all statuses used here and that storage failures leave the record non-ready. In a production design, consider writing to a temporary path and moving to the final path after the write is complete, so a retry or concurrent request cannot mistake a partial file for a finished one. Make retry behavior idempotent and clean up abandoned temporary files.

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

Return the stored PDF after authorization

public function download(Request $request, PdfDownload $pdfDownload)
{
    $this->authorize('view', $pdfDownload);

    abort_unless($pdfDownload->status === 'ready', 404);
    abort_unless($pdfDownload->disk && $pdfDownload->path, 404);

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

Register this action behind authentication as appropriate and enforce the policy before returning the file. Laravel’s download method generates a response that forces the browser to download the file at the given path; it accepts a filename and headers. If instead you return a temporary URL, authorize before generating it and choose an expiry that matches the use case.

Run workers and set recovery behavior

Dispatching a job is not enough: a worker must consume the matching connection and queue. Run the worker under your deployment’s process manager or managed worker service, and monitor it along with failed jobs and storage errors. Laravel documents worker timeouts, attempts, retries, and failed-job handling; their values need to reflect your source’s response times, file sizes, worker resources, and queue backend.

  • Bound request timeouts so an upstream stall does not occupy a worker indefinitely.
  • Set retry and backoff behavior for transient failures, and avoid retrying permanent problems such as an invalid source or authorization denial without changing inputs.
  • Coordinate job timeout with the queue connection’s retry or visibility interval. If a job can be considered lost and redelivered while its first worker is still running, duplicate execution is possible.
  • Record useful failure states and diagnostics, but do not expose internal exception details or sensitive source credentials to users.
  • Make repeated execution safe: use stable record IDs, deterministic state transitions, and cleanup or replacement rules for files.

There is no universal timeout, retry count, or file-size threshold appropriate for every deployment. Measure your own upstream and storage behavior before choosing operational values.

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

Common failures and fixes

The browser waits instead of receiving an immediate response

The request may be doing the remote fetch inline, or the application may be using a synchronous queue connection. Return a queued status/identifier, use an asynchronous connection, and confirm a worker is consuming the selected queue.

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

The record stays queued or processing

Check that a worker is running with the same connection and queue name used at dispatch, that deployment configuration is current, and that failed jobs are visible. A status update should occur after each meaningful transition, and failure handling should not accidentally leave the record indefinitely processing.

A job repeatedly fails on timeout

Check upstream latency, the configured HTTP timeout, job timeout, worker resource limits, and queue visibility/retry interval together. Do not simply raise every timeout: first determine whether the remote host is slow, the body is too large for buffering, or the worker lacks memory or network access.

A downloaded file is HTML, empty, or corrupt

A successful HTTP status is not sufficient. Check content type and body signature, account for redirects and upstream login pages, enforce a sensible size limit, and validate the downloaded file using requirements suitable for your application. Treat a rejected response as a failure rather than marking the record ready.

The user gets a forbidden response or another user’s PDF

Check the record policy and ownership relation before producing either a storage response or temporary URL. Avoid public storage for confidential documents; use private storage and a controlled access path.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Duplicate files or partial downloads appear after retries

Use an idempotent job and a temporary-write/finalize pattern. Ensure the completion status is written only after the stored object is complete, and coordinate worker timeout with backend redelivery behavior.

Download versus generate a PDF

This flow retrieves an existing remote file. Generating a PDF from a Blade view or browser-rendered page is a different job: generation creates the document, while this tutorial fetches bytes that already exist. Spatie’s Laravel PDF package page discusses queued PDF generation, but it is not automatically the right abstraction for downloading an existing remote PDF; verify its current API and constraints before adopting it. Spatie queued PDF generation documentation.

Or skip the browser setup

If the PDF you need is actually a rendered webpage, ScreenshotNeo can capture a page as a PDF without running a browser in your own worker. For an existing remote PDF file, keep the Laravel fetch-and-store workflow above; a webpage screenshot API is not a replacement for retrieving arbitrary PDF files.

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 PDF capture parameters and other options. ScreenshotNeo accepts cookie and consent banners and removes known consent platforms, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed. Its MCP server lets AI agents take screenshots, and the free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month with no card.

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

Frequently Asked Questions

Does dispatching a Laravel job make the PDF download begin in the same request?

No. The job retrieves and stores the file separately; the user needs a later authorized request or suitable temporary link to receive it.

Can I use this pattern for PDFs my application generates?

Generation is a distinct workflow. Generate the file in a job, store it, and then expose it through the same kind of authorized completion-and-download flow.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.