ScreenshotNeo

BlogHow-to

Use Laravel Browsershot to Generate Website Screenshot Thumbnails in a Queue

Move website thumbnail rendering out of web requests with a Laravel queued job, a worker-ready Browsershot setup, and clear failure handling.

By the ScreenshotNeo team4 October 20269 min read

Use a Laravel queued job to run Browsershot in the background. The job receives a URL and a server-generated output path, renders the page with headless Chrome, and saves an image. A queue worker must have access to Node.js, Puppeteer, and Chrome or Chromium. This keeps browser rendering out of the normal web request; it does not remove the need to configure and monitor the worker.

The example below uses Laravel’s queue job pattern and Browsershot’s url(...)->save(...) API. It is a combination of those documented APIs, not a tested, package-provided recipe. Confirm compatibility and configuration against the Laravel and Browsershot versions installed in your application. See the Laravel queues documentation and Spatie Browsershot documentation.

1. Install and prepare the browser runtime

Install Browsershot in the Laravel project using its installation instructions. Browsershot controls headless Chrome through Puppeteer, so the environment that runs the queue worker needs the required Node.js, Puppeteer, and Chrome or Chromium dependencies. Installing them only on a web server does not help if workers run in a separate container or machine.

Before dispatching jobs, decide where the rendered files will live. This example writes to Laravel’s local storage directory. That works when the application and worker share the relevant filesystem. If workers run on separate hosts or containers, use shared or object storage and persist a durable storage reference for the rest of the application to retrieve.

2. Create a queued screenshot job

Generate a job with Artisan:

php artisan make:job GenerateWebsiteThumbnail

Replace the generated class with the following. The caller supplies a URL and an opaque identifier; the job constructs the output path itself so callers cannot choose arbitrary filesystem locations.

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Spatie\Browsershot\Browsershot;
use Throwable;

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

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

    public function __construct(
        public string $url,
        public string $thumbnailId,
    ) {
        $this->onQueue('screenshots');
    }

    public function handle(): void
    {
        $directory = storage_path('app/public/thumbnails');
        $path = $directory . '/' . $this->thumbnailId . '.png';

        if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
            throw new \RuntimeException('Could not create the thumbnail directory.');
        }

        Browsershot::url($this->url)
            ->windowSize(1200, 630)
            ->save($path);

        // Persist the storage path or mark the related record as complete here.
    }

    public function failed(Throwable $exception): void
    {
        Log::error('Website thumbnail generation failed.', [
            'thumbnail_id' => $this->thumbnailId,
            'url' => $this->url,
            'exception' => $exception->getMessage(),
        ]);

        // Update the related record to a failed state or notify the application here.
    }
}

windowSize() sets the viewport used for the thumbnail. Change the dimensions to match the card or preview in your application. Browsershot supports additional browser and screenshot settings; use the options documented for your installed release. Keep job properties serializable: pass scalar values or simple data rather than browser objects, closures, or open resources.

For production applications, validate the URL before dispatching and consider whether your service is allowed to fetch arbitrary hosts. A user-controlled URL can point at internal services or other sensitive network destinations. Apply an allowlist or other outbound request policy appropriate to your application. Generate identifiers on the server, and do not accept a filesystem path from the request.

3. Validate input and dispatch the job

For example, a controller can validate the submitted URL, generate a UUID for the thumbnail, and queue the work. Add your application’s host policy before dispatching if users can choose the destination.

<?php

namespace App\Http\Controllers;

use App\Jobs\GenerateWebsiteThumbnail;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

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

        $thumbnailId = (string) Str::uuid();

        GenerateWebsiteThumbnail::dispatch($data['url'], $thumbnailId);

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

A 202 Accepted response means the application accepted the work for processing; it does not mean the screenshot is ready. Store a record with a pending status if clients need to poll for completion. In handle(), update that record after a successful save; in failed(), record the terminal failure. Make those updates idempotent because a job may be attempted again after an interruption.

4. Configure a queue worker

Configure a Laravel queue connection in the environment and application configuration, then start a worker that listens to the queue selected by the job:

php artisan queue:work --queue=screenshots,default

The worker process must run in an environment with the browser dependencies and access to the output destination. Keep it supervised in production so it restarts after exit, and restart workers during deployments when application code changes. Laravel’s queue connection, worker, retry, and timeout behavior is described in its queue documentation. The cited page is for Laravel 12; use the documentation matching your installed release.

Set job timeout and retry behavior based on the pages you capture and the limits of your infrastructure. The sample uses a 120-second timeout and three attempts only as values to replace, not as universal recommendations. Check that the worker timeout, queue connection’s retry-after setting, and any process supervisor limits agree. Laravel documents the relationship and configuration options; avoid allowing the queue to make a job visible for retry while the original worker may still be running it.

5. Store and serve the completed thumbnail

The sample writes a PNG beneath storage/app/public/thumbnails. If using Laravel’s public storage disk, configure its public symlink as documented for your application, or serve the image through an authorized controller. Avoid exposing screenshots publicly if the captured pages or query strings may contain private data.

For object storage, write the screenshot to a worker-accessible temporary file, then upload it through Laravel’s filesystem API and save the resulting disk and key in your database. Ensure temporary files are cleaned up on both success and failure. Do not assume a local path on one worker is visible to another worker or to the web process.

6. Choose direct Browsershot or the Laravel Screenshot wrapper

A custom job is useful when you want control over job data, application state transitions, retry policy, and the exact Browsershot call. Spatie’s Laravel Screenshot v1 also provides a queued facade for projects that want its built-in queue and storage integration:

use Spatie\LaravelScreenshot\Screenshot;

Screenshot::url($url)->saveQueued('screenshots/homepage.png');

That wrapper documents queue and connection selection, disk selection, and success and failure callbacks. See Queued screenshot generation and its Browsershot driver configuration. The wrapper’s queued call cannot be combined with withBrowsershot(): its customization closure cannot be serialized for the queue. If you need that customization, use a supported wrapper configuration or make a custom queued job that calls Browsershot directly. Confirm the exact API for the version installed.

7. Handle reliability, performance, and cost

  • Keep the web request short. Dispatch the job and return an identifier or pending resource. The user interface can poll, subscribe to an application event, or check later for completion.
  • Isolate browser work. A named queue lets you run screenshot jobs on workers sized and configured for browser processes. Monitor queue depth and failures so a slow or inaccessible destination does not silently accumulate work.
  • Set realistic timeouts. Page load time, scripts, network access, and image size vary. Set limits from your workload, align Laravel’s job timeout with the queue connection’s retry-after value, and avoid unlimited retries for permanent errors.
  • Make retries safe. Use a deterministic output key per thumbnail record, update status transactionally where possible, and make repeated completion handling safe. Clean up partial files when a capture fails.
  • Limit resource use. Headless browsers consume memory and CPU. Bound worker concurrency, avoid starting more simultaneous browser jobs than the host can support, and consider a separate queue for captures.
  • Control destination access. Validate schemes and hosts, prevent access to private network ranges as appropriate, and avoid forwarding application secrets in capture URLs or headers.
  • Budget infrastructure. The dossier identifies no universal throughput or cost figure for a local Browsershot deployment. Your costs depend on worker resources, concurrency, page behavior, storage, and operational needs; measure those in your environment.

8. Troubleshoot common failures

Symptom Likely cause What to check or change
Job stays pending No worker is listening to the configured connection or queue. Check the queue connection, worker process, selected queue name, and failed-job table. Start a worker with the queue the job uses.
Node, Puppeteer, or Chrome executable not found The worker runtime lacks a dependency or uses a different binary path than the web process. Install and configure dependencies in the worker image or host. Verify executable paths and permissions as the worker user.
Chrome exits immediately in a container Container restrictions, missing system dependencies, or sandbox configuration prevent startup. Check the browser’s stderr and the Browsershot configuration for your environment. Browsershot and the wrapper expose executable and path configuration; a no_sandbox setting is deployment-specific and has security implications, so do not enable it blindly.
Job times out on some pages The page takes longer to load, waits on external resources, or runs expensive scripts. Inspect the destination and capture settings, choose appropriate wait behavior and timeout limits, and align worker and queue retry settings. Do not repeatedly retry a page that consistently exceeds policy.
Screenshot exists on worker but cannot be served The output is on a local filesystem unavailable to the web process, or the public storage mapping is missing. Use shared/object storage or a common volume, check permissions, and verify the configured disk and public-serving path.
Duplicate work or overwritten thumbnails A retry ran after an uncertain completion, or multiple jobs used the same output key. Use a unique server-generated identifier, make status updates idempotent, and decide whether overwriting the same record’s key is acceptable.
Queued wrapper rejects custom Browsershot setup saveQueued() cannot serialize the withBrowsershot() closure. Use the wrapper’s supported configuration, or put the Browsershot call in your own queued job.

9. Or skip the browser setup

If you do not want to provision Node.js, Puppeteer, and Chrome on your workers, ScreenshotNeo provides a screenshot API. One GET request returns an image or PDF, so your Laravel job can call an HTTP endpoint instead of running a local browser. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

In a Laravel job, make the same authenticated GET request with your HTTP client, check the response, and store the returned bytes using Laravel’s filesystem. Keep the API key in server-side configuration, not in browser code. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for the free plan.

Frequently asked questions

Can I queue a screenshot of HTML instead of a URL?

Browsershot supports rendering HTML as well as URLs. For queued work, pass serializable HTML or a durable reference to it, and confirm the HTML rendering API and asset handling for your installed Browsershot version.

Can a queued job return the image to the original HTTP response?

Not after the request has already returned. Return a job or thumbnail identifier, then expose completion through polling, events, or another application workflow.

Does the Laravel Screenshot wrapper use Browsershot?

The v1 package documents a driver-based setup with Browsershot support. Verify the package version and selected driver in your application before relying on that configuration.

Can I use Cloudflare instead of hosting Chrome?

Cloudflare documents Browser Run, formerly Browser Rendering, with screenshot capabilities. It is a hosted browser option to evaluate if you cannot maintain a local browser runtime; it is a separate integration, not an identical drop-in Browsershot API. See the Cloudflare Browser Run documentation.