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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use context.WithTimeout at the PDF-generation boundary, always defer its cancel function, and pass the derived context into every renderer and sub-operation that accepts one. The effective deadline is whichever comes first: the new timeout or the parent context’s existing deadline.

func GeneratePDF(parent context.Context, input Input) ([]byte, error) {
    ctx, cancel := context.WithTimeout(parent, 10*time.Second)
    defer cancel()

    return renderer.Generate(ctx, input)
}

10*time.Second is only an example. Select a limit from your service’s latency objective and measurements of representative documents. A context deadline is a cancellation signal, not a universal kill switch: generation stops promptly only when the renderer and the blocking operations it calls observe the context.

What a Go PDF timeout actually does

context.Context carries deadlines and cancellation signals across API boundaries. context.WithTimeout(parent, limit) creates a child context that is canceled when limit elapses, when the parent is canceled, or when the parent reaches an earlier deadline. It can never extend the parent’s deadline.

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

The cancellation signal is cooperative. Code waiting on a context-aware operation can return; arbitrary CPU-bound code, a library that ignores the context, or a system call with no cancellation support may continue running. Therefore, a timeout bounds the parts of the pipeline that honor the context, not necessarily every instruction in the process.

Basic implementation

Accept the caller’s context

Make the context an explicit parameter instead of creating context.Background() inside request or job code. Using Background() there severs cancellation from the caller and can leave work running after an HTTP client disconnects or a job is canceled.

package pdf

import (
    "context"
    "errors"
    "fmt"
    "time"
)

type Input struct {
    Title string
    Body  string
}

type Renderer interface {
    Generate(context.Context, Input) ([]byte, error)
}

func GeneratePDF(parent context.Context, renderer Renderer, input Input) ([]byte, error) {
    if parent == nil {
        return nil, errors.New("nil parent context")
    }

    ctx, cancel := context.WithTimeout(parent, 10*time.Second)
    defer cancel() // releases resources on every return path

    pdf, err := renderer.Generate(ctx, input)
    if err != nil {
        if errors.Is(ctx.Err(), context.DeadlineExceeded) {
            return nil, fmt.Errorf("PDF generation exceeded its deadline: %w", err)
        }
        if errors.Is(ctx.Err(), context.Canceled) {
            return nil, fmt.Errorf("PDF generation canceled: %w", err)
        }
        return nil, err
    }
    return pdf, nil
}

In production, use a timeout chosen for your workload rather than copying the example. Calling cancel is required even when generation succeeds; it releases resources associated with the derived context, and tools such as go vet check that cancel functions are used on all control-flow paths.

Propagate the context through every stage

Pass the same derived context to template rendering, database reads, remote asset downloads, font loading, image decoding, and the final PDF operation whenever those APIs accept a context. A timeout applied only around the final function does not limit an earlier, uncancellable preparation step.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
func BuildPDF(parent context.Context, r Renderer, data DataStore, assets AssetStore) ([]byte, error) {
    ctx, cancel := context.WithTimeout(parent, 15*time.Second)
    defer cancel()

    model, err := data.Load(ctx)
    if err != nil {
        return nil, err
    }
    images, err := assets.Fetch(ctx, model.ImageURLs)
    if err != nil {
        return nil, err
    }
    return r.Generate(ctx, Input{Model: model, Images: images})
}

If an older dependency has no context parameter, isolate it and document that the timeout cannot interrupt that call. You may be able to put the operation in a worker process and terminate that process, but that is an architectural choice with its own cleanup and data-loss consequences; a context alone does not provide forced termination.

HTTP handler pattern

An incoming HTTP request already has a context. Use r.Context() as the parent so a client disconnect, server cancellation, or upstream deadline propagates automatically. Derive a PDF-specific limit that is shorter when that is appropriate for your endpoint.

func pdfHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
    defer cancel()

    pdf, err := GeneratePDFWithContext(ctx, r)
    if err != nil {
        switch {
        case errors.Is(ctx.Err(), context.DeadlineExceeded):
            http.Error(w, "PDF generation timed out", http.StatusGatewayTimeout)
        case errors.Is(ctx.Err(), context.Canceled):
            // The client may already have gone away; avoid writing a response.
            return
        default:
            http.Error(w, "PDF generation failed", http.StatusInternalServerError)
        }
        return
    }

    w.Header().Set("Content-Type", "application/pdf")
    w.Header().Set("Content-Disposition", "attachment; filename=report.pdf")
    _, _ = w.Write(pdf)
}

Do not classify every renderer error as a timeout. Check ctx.Err() (or the returned error if the library wraps the context error) and distinguish context.DeadlineExceeded, context.Canceled, and ordinary generation failures.

Choosing a deadline without guessing

Neither Go’s context package nor the PDF libraries discussed here defines a universal “correct” duration. Set a service objective first, then measure representative workloads: page count, fonts, image sizes, remote resources, templates, and concurrency all affect latency.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Measure successful and failed jobs separately, including the slowest realistic documents.
  • Leave room for queueing, network overhead, and response transmission when the timeout is for an HTTP request.
  • Use a shorter child deadline for a sub-operation when it must leave time for cleanup or a subsequent stage.
  • Record the configured duration and the stage that was active when cancellation occurred.

A single fixed value may be unsuitable for both a one-page invoice and a large, image-heavy report. Where policy permits, derive a bounded duration from workload characteristics, but always enforce an upper limit.

Browser-backed PDF generation with chromedp

When Chrome drives rendering through chromedp, create its context from the request or job context rather than from context.Background(). The package documents cancellation as closing a tab or browser. Its cancellation function waits for cleanup, so browser shutdown deserves a separate bound from the render deadline.

func RenderChrome(parent context.Context, url string) ([]byte, error) {
    renderCtx, cancelRender := context.WithTimeout(parent, 30*time.Second)
    defer cancelRender()

    browserCtx, cancelBrowser := chromedp.NewContext(renderCtx)
    defer cancelBrowser()

    cleanupCtx, cancelCleanup := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancelCleanup()

    var pdf []byte
    err := chromedp.Run(browserCtx,
        chromedp.Navigate(url),
        chromedp.WaitReady("body"),
        chromedp.PrintToPDF(&pdf),
    )
    if err != nil {
        return nil, err
    }
    return pdf, nil
}

The exact shutdown behavior depends on the chromedp version and browser state. A render timeout is not proof that Chrome stopped instantaneously. Verify whether tabs, browser processes, temporary profiles, and partial output are cleaned up in your deployment, and keep cleanup bounded so a stuck browser does not consume a worker forever.

Library support is not uniform

Context-aware libraries

Some Go PDF packages expose context-aware operations. The pdfcpu API, for example, documents application-context parameters and cancellation support for CreateFile. With such an API, pass your derived context directly and test how cancellation affects files that are already being written.

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

Libraries without context parameters

Many renderers expose synchronous methods that accept no context. Wrapping those methods in WithTimeout does not interrupt them; it only lets surrounding code notice that the deadline has passed. Avoid returning a response while an uncancellable worker continues to consume unbounded resources. Consider a worker process with an operating-system-level kill policy when hard isolation is required, and define what happens to temporary files and partial PDFs.

Partial output, cleanup, and retries

Decide explicitly whether a timed-out result is discarded, retried, or retained for diagnosis. The context package does not prescribe PDF-library file semantics. Use temporary paths, write atomically to the final location only after successful completion, and remove temporary files on every failure path. A retry should have a new, deliberately budgeted context; blindly retrying a slow document can multiply load and still exceed the caller’s deadline.

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

Testing timeout behavior

  • Use a fake renderer that blocks on <-ctx.Done() to verify the deadline and error classification.
  • Test parent cancellation before the child timeout and confirm that context.Canceled is reported.
  • Test an earlier parent deadline; the child must not outlive it.
  • Test a renderer that ignores context so your system’s resource and response policy is visible.
  • For browser rendering, test browser-process cleanup and temporary-profile deletion after cancellation.
type blockingRenderer struct{}

func (blockingRenderer) Generate(ctx context.Context, _ Input) ([]byte, error) {
    <-ctx.Done()
    return nil, ctx.Err()
}

func TestGeneratePDFTimeout(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
    defer cancel()

    _, err := GeneratePDF(ctx, blockingRenderer{}, Input{})
    if !errors.Is(err, context.DeadlineExceeded) {
        t.Fatalf("expected deadline exceeded, got %v", err)
    }
}

Common errors and fixes

Symptom Likely cause Fix
The function keeps running after the deadline The renderer or a nested operation never checks the context. Use a context-aware API, add cancellation checks around your own loops, or isolate the renderer in a killable worker process.
Client disconnects do not stop work The handler started from context.Background(). Derive from r.Context() and pass that context through every stage.
go vet reports a lost cancel The cancel function is not called on every path. Call defer cancel() immediately after creating the child context.
Every failure is labeled “timeout” Error classification relies only on the renderer’s error. Inspect ctx.Err() and preserve non-context errors.
Chrome processes accumulate Browser cancellation or cleanup is unbounded or skipped. Defer browser cancellation, apply a separate cleanup deadline, and verify behavior for your chromedp version.
A partial PDF is served Output was written directly to its final destination. Write to a temporary file or buffer and publish atomically only after success.

Or skip the browser setup

If your goal is simply to obtain a clean PDF or screenshot from a URL, ScreenshotNeo provides a single HTTP call instead of maintaining Chrome. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

Documentation: ScreenshotNeo API docs. The following request returns a PDF when the target and options are set accordingly; use your API key and target URL.

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.
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(`HTTP ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', body);

ScreenshotNeo includes full-page capture, CSS-selector element capture, device and viewport controls, PDF paper and margin options, custom JavaScript and CSS, waits, request blocking, authentication headers and cookies, geolocation, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and a usage API. The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo or sign up free.

Frequently Asked Questions

How do I stop PDF generation if it takes too long?

Derive a child context with context.WithTimeout, pass it to a renderer that observes cancellation, and handle context.DeadlineExceeded. An uncancellable renderer may continue running.

Does context.WithTimeout stop a Go function?

No. It broadcasts cancellation. The function or library must check the context or use context-aware blocking operations; otherwise the deadline cannot forcibly interrupt it.

What happens if the parent context already has a shorter deadline?

The earlier deadline wins. A child created with WithTimeout cannot extend the lifetime granted by its parent.

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.