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
Amazon S3

How to Use PDFKit in AWS Lambda (Node.js)

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

Use PDFKit as a normal Node.js dependency, finish the document stream, and return the resulting bytes as base64 for an API response. For files that must persist, write the PDF to Lambda’s writable /tmp directory and upload it to Amazon S3 before the invocation ends. Package any custom fonts in the deployment artifact and resolve their paths from the function bundle.

This guide covers synchronous downloads, S3-backed workflows, fonts, deployment, API Gateway responses, event-driven generation, performance, and recovery from common failures.

What the Lambda architecture looks like

PDFKit is a JavaScript PDF-generation library for Node.js and browsers. In Lambda, the Node build emits PDF data through a stream. Your handler collects those chunks (or writes them to a file), calls doc.end(), and then returns or uploads the completed PDF.

  • Small, immediate document: collect stream chunks in memory and return a base64-encoded proxy response.
  • Durable, larger, or asynchronous document: write to /tmp, upload to S3, and return an object key or download link through your surrounding application.
  • Event-driven workflow: invoke the function from an S3 event or another queue-oriented service when generation should be decoupled from a user request.

Lambda’s writable temporary filesystem is /tmp; it is not durable storage. Copy anything that must survive the invocation to S3.

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

Package PDFKit and deploy it with Lambda

Install the dependency

mkdir pdf-lambda
cd pdf-lambda
npm init -y
npm install pdfkit

Keep pdfkit in dependencies, not only in devDependencies. Lambda must receive the dependency in the zip artifact; it cannot rely on the node_modules directory on your laptop.

Create the deployment zip

  1. Place your handler file (for example, index.js) and package.json in the project directory.
  2. Run npm install in that directory so node_modules/pdfkit is present.
  3. Zip the project contents, including node_modules, and upload the archive using the Lambda console, CLI, or your deployment pipeline.
  4. Set the handler to index.handler and choose a Node.js Lambda runtime supported by your account.

The AWS Node.js runtime includes common libraries and the AWS SDK for JavaScript, but your application dependencies still belong in the artifact unless you deliberately provide them through a layer.

Return a PDF directly from a Lambda URL or API Gateway

The following handler creates a document, waits for PDFKit’s stream to finish, and returns a response compatible with API Gateway-style proxy integrations and Lambda URLs.

const PDFDocument = require('pdfkit');

exports.handler = async () => {
  const doc = new PDFDocument();
  const chunks = [];

  doc.on('data', chunk => chunks.push(chunk));
  const done = new Promise((resolve, reject) => {
    doc.on('end', resolve);
    doc.on('error', reject);
  });

  doc.fontSize(20).text('Hello from AWS Lambda');
  doc.moveDown().fontSize(12).text(`Created: ${new Date().toISOString()}`);
  doc.end();

  await done;
  const pdf = Buffer.concat(chunks);

  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'inline; filename="document.pdf"'
    },
    isBase64Encoded: true,
    body: pdf.toString('base64')
  };
};

doc.end() is mandatory: it tells PDFKit that no more content is coming and allows the end event to fire. The response body is binary data encoded as base64, and isBase64Encoded: true tells the proxy integration how to decode it.

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

Adding multiple pages

const PDFDocument = require('pdfkit');

exports.handler = async (event) => {
  const doc = new PDFDocument({ size: 'A4', margin: 50 });
  const chunks = [];
  const done = new Promise((resolve, reject) => {
    doc.on('data', chunk => chunks.push(chunk));
    doc.on('end', resolve);
    doc.on('error', reject);
  });

  doc.fontSize(18).text(event.title || 'Report');
  doc.moveDown();
  for (const paragraph of (event.paragraphs || [])) {
    doc.fontSize(11).text(String(paragraph), { paragraphGap: 8 });
  }
  doc.addPage().fontSize(18).text('Second page');
  doc.end();
  await done;

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/pdf' },
    isBase64Encoded: true,
    body: Buffer.concat(chunks).toString('base64')
  };
};

Write the PDF to /tmp and upload it to S3

Use this pattern when a PDF must be retained, is too large for a practical synchronous response, or will be consumed by several downstream services. The AWS file-processing pattern uses /tmp for intermediate files and S3 for the destination object.

const PDFDocument = require('pdfkit');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const fs = require('node:fs');
const path = require('node:path');

const s3 = new S3Client({});
const BUCKET = process.env.DESTINATION_BUCKET;

exports.handler = async (event) => {
  if (!BUCKET) throw new Error('DESTINATION_BUCKET is not configured');

  const key = `reports/${event.id || Date.now()}.pdf`;
  const filePath = path.join('/tmp', `report-${Date.now()}.pdf`);
  const doc = new PDFDocument({ size: 'A4', margin: 50 });
  const output = fs.createWriteStream(filePath);

  const done = new Promise((resolve, reject) => {
    output.on('finish', resolve);
    output.on('error', reject);
    doc.on('error', reject);
  });

  doc.pipe(output);
  doc.fontSize(20).text(event.title || 'Report');
  doc.moveDown().fontSize(11).text(event.body || '');
  doc.end();
  await done;

  await s3.send(new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
    Body: fs.createReadStream(filePath),
    ContentType: 'application/pdf'
  }));

  return { statusCode: 200, body: JSON.stringify({ bucket: BUCKET, key }) };
};

Install the S3 client package if it is not supplied by your deployment strategy: npm install @aws-sdk/client-s3. Grant the Lambda execution role s3:PutObject on the destination prefix. If the function is triggered by an S3 upload, write the generated object to a different key or bucket so the output does not recursively trigger the same function.

Fonts: standard PDF fonts versus bundled files

Use a standard font without packaging files

PDFKit supports the 14 standard PDF fonts, including Helvetica, Courier, Times, Symbol, and ZapfDingbats. They minimize deployment size, but they do not provide your brand typeface or broad multilingual glyph coverage.

doc.font('Helvetica').fontSize(12).text('Standard font text');

Embed a TTF or OTF font

For branding, non-Latin scripts, or accessibility requirements, include a .ttf or .otf file in the zip and register it using a path relative to the deployed bundle.

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.
const path = require('node:path');
const PDFDocument = require('pdfkit');

const doc = new PDFDocument();
const fontPath = path.join(__dirname, 'fonts', 'Brand-Regular.ttf');
doc.registerFont('Brand', fontPath);
doc.font('Brand').fontSize(14).text('Branded and multilingual text');

Do not reference a path that exists only on your development machine. A font downloaded during an invocation can be placed in /tmp, then loaded from there; package stable fonts with the function whenever possible. Embedded TrueType or OpenType fonts are the appropriate choice when a compliant PDF requires predictable glyphs.

Choose the invocation and storage model

Requirement Recommended flow Important trade-off
Immediate download of a small PDF Collect stream chunks and return base64 Memory use grows with document size and proxy payload limits apply
Durable archive or large output Stream to /tmp, upload to S3 Requires IAM permissions and cleanup/ key management
Generation after an upload or batch event S3-triggered or queue-driven Lambda Caller receives an object key or status, not the PDF inline

For fan-out workflows, have the surrounding application return a status identifier and then provide an S3 download or presigned URL after generation completes. Keep the generated key deterministic when retries should overwrite the same logical document, or include a unique ID when every attempt must be retained.

Performance and reliability practices

  • Generate only the content needed for the request; large in-memory buffers increase peak memory and can cause out-of-memory termination.
  • For file workflows, stream PDFKit output to a file rather than retaining every chunk in an array.
  • Reuse a warm S3 client outside the handler so connections can be reused across invocations.
  • Set Lambda memory and timeout for the document size, embedded fonts, and S3 transfer time; verify these settings with your own workload.
  • Use unique temporary filenames because warm environments may retain files from a previous invocation. Remove files after upload when practical.
  • Handle retries idempotently. An S3 event can be delivered more than once, so do not assume one invocation per source object.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting PDFKit on Lambda

The invocation hangs or never returns

Call doc.end() after the final drawing operation and await the stream’s end or file output completion event. Also attach an error listener so a stream failure rejects the handler instead of leaving it waiting.

The client downloads a corrupt PDF

Return Buffer data as a base64 string and set isBase64Encoded: true. A raw binary string sent through an API Gateway proxy response can be altered. Confirm the Content-Type is application/pdf.

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

The custom font works locally but fails in Lambda

Check that the font file is inside the zip, that its filename casing matches exactly, and that the code builds the path with __dirname (or another bundle-relative path). Do not assume your local working directory or home directory exists in Lambda.

The S3 object disappears after the function finishes

That is expected if the file exists only in /tmp. Upload it to S3 before returning and treat S3 as the durable copy.

Large documents exhaust memory

Switch from chunk collection to doc.pipe(fs.createWriteStream('/tmp/...')), increase the function’s memory setting when justified, and upload the resulting file. Avoid embedding unnecessarily large images or fonts.

An S3 trigger loops repeatedly

Write generated PDFs to a destination prefix or bucket that is excluded from the trigger. Filter event rules so the function responds only to the intended source objects.

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

Or skip the browser setup

If your actual requirement is a rendered screenshot or PDF of a live webpage rather than a programmatically composed document, ScreenshotNeo provides a single HTTP call. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

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 documentation for output and option details. The service includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

FAQ

Can PDFKit run in a Lambda layer?

Yes. A layer can provide shared dependencies, but the runtime must be able to resolve the package and any native or font files at the paths your code uses. A zip deployment that contains pdfkit is the simplest arrangement.

Should I generate PDFs synchronously?

Use synchronous generation when the caller needs a small document immediately. Choose S3-backed asynchronous processing when the output is large, durable, or produced from an event.

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

Which font should I choose for accessibility?

Use an embedded TrueType or OpenType font with the glyph coverage your content needs, and validate the finished PDF with the accessibility requirements of your target workflow.

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
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

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.