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

Generate the PDF, finish the PDF stream, and await an Amazon S3 upload. For a small or moderate document, create PDFKit output in memory and send the resulting Buffer with AWS SDK for JavaScript v3’s PutObjectCommand. For larger documents, stage the file or use the SDK v3 multipart helper, @aws-sdk/lib-storage. In every case, configure credentials and the bucket’s actual Region, set ContentType to application/pdf, and report success only after the upload promise resolves.

What you need before writing code

  • An Active LTS release of Node.js, as recommended in AWS’s Node.js guidance.
  • An AWS identity whose permissions allow the intended s3:PutObject operation on the target bucket and key prefix.
  • The bucket name and its Region. Configure that Region explicitly in deployment rather than relying on a developer machine’s default.
  • Packages installed in your project:
npm install pdfkit @aws-sdk/client-s3 @aws-sdk/lib-storage

Use only the packages needed by your chosen transfer path. Keep credentials out of source control; the AWS SDK can use its standard credential provider chain (environment variables, role credentials and local AWS configuration).

Generate a PDF and upload a buffer with PutObject

PDFKit’s PDFDocument is a readable Node.js stream. It does not write a complete file until you call doc.end(). The following self-contained ES module creates a simple report, collects the emitted chunks, and uploads the finished bytes.

import PDFDocument from "pdfkit";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const region = process.env.AWS_REGION;
const bucket = process.env.PDF_BUCKET;
const key = process.env.PDF_KEY ?? `reports/report-${Date.now()}.pdf`;

if (!region || !bucket) {
  throw new Error("AWS_REGION and PDF_BUCKET are required");
}

function makePdfBuffer() {
  return new Promise((resolve, reject) => {
    const doc = new PDFDocument();
    const chunks = [];

    doc.on("data", chunk => chunks.push(chunk));
    doc.once("end", () => resolve(Buffer.concat(chunks)));
    doc.once("error", reject);

    doc.fontSize(22).text("Monthly report", { align: "center" });
    doc.moveDown().fontSize(12).text(`Generated: ${new Date().toISOString()}`);
    doc.moveDown().text("This PDF was generated by Node.js and stored in Amazon S3.");
    doc.end();
  });
}

const pdfBuffer = await makePdfBuffer();
const s3 = new S3Client({ region });

try {
  const result = await s3.send(new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    Body: pdfBuffer,
    ContentType: "application/pdf"
  }));

  console.log({ bucket, key, etag: result.ETag });
} catch (error) {
  console.error("S3 PDF upload failed", {
    name: error.name,
    message: error.message,
    bucket,
    key
  });
  throw error;
}

Run this as an ES module (for example, set "type": "module" in package.json) and provide AWS_REGION, PDF_BUCKET, and optionally PDF_KEY. A successful send means the S3 request completed; the returned ETag is useful for logging but should not automatically be treated as a content hash for every upload mode.

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

Why these request fields matter

  • Bucket identifies the destination bucket.
  • Key is the complete object name, including any prefix such as reports/.
  • Body accepts the generated bytes.
  • ContentType: "application/pdf" lets browsers and downstream services handle the object as a PDF.

Choose an intentional key and keep the bucket private unless your application’s access policy specifically requires public access. A PDF being easy to retrieve is not a reason to make an entire bucket or object public.

Do you need to save the PDF to disk?

No. You can upload a buffer directly, or use a stream-oriented design. The right choice depends on document size, memory limits, retry requirements and operational complexity.

Approach Peak memory Disk use When it fits Main concern
Buffer plus PutObject PDF bytes are held in memory None Small or moderate reports and simple workers A large PDF can pressure the process heap
Temporary file plus read stream Small application buffer Temporary file required When memory is constrained and local ephemeral storage is available Clean up files on success, failure and cancellation
PDFKit stream plus multipart helper Designed to avoid one giant buffer None in a direct pipeline Large output or long-running generation More involved stream, retry and backpressure handling

The table describes engineering trade-offs, not a published performance benchmark. Test with your document sizes and runtime limits.

Upload a generated stream with multipart support

A PDFKit stream can feed an upload, but do not assume that two readable-stream interfaces compose correctly without testing. The producer must be finalized, stream errors must reject the upload, backpressure must be respected, and the upload promise must be awaited. AWS identifies @aws-sdk/lib-storage as the SDK v3 multipart-upload helper.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import PDFDocument from "pdfkit";
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";

const doc = new PDFDocument();
const s3 = new S3Client({ region: process.env.AWS_REGION });

const upload = new Upload({
  client: s3,
  params: {
    Bucket: process.env.PDF_BUCKET,
    Key: "reports/large-report.pdf",
    Body: doc,
    ContentType: "application/pdf"
  }
});

upload.on("httpUploadProgress", progress => {
  console.log({ loaded: progress.loaded, total: progress.total });
});

const uploadPromise = upload.done();

doc.on("error", error => upload.abort().catch(() => {}));
doc.fontSize(18).text("Large report");
// Add the rest of the document here, then finalize the readable stream.
doc.end();

try {
  await uploadPromise;
  console.log("PDF upload complete");
} catch (error) {
  console.error("Multipart PDF upload failed", error);
  throw error;
}

This pattern is intentionally a starting point rather than a guarantee for every installed version. Verify the exact SDK and PDFKit versions, test a forced stream error, and confirm that cancellation does not leave an incomplete multipart upload. If that complexity is not justified, write to a temporary file and upload a read stream, or use the buffer pattern.

Temporary-file workflow

Staging is often the easiest compromise for a large report: PDFKit writes to a temporary file, the file is opened as a read stream, and the stream is passed to the S3 operation or multipart helper. Use a unique path, restrict its permissions, remove it in a finally block, and account for the container or serverless environment’s ephemeral-storage limit. Do not expose the temporary path or document contents in ordinary logs.

Region, credentials and integrity

Configure the bucket’s Region

Construct S3Client with the Region that actually contains the bucket. A wrong Region can produce redirects or authorization-looking failures. Make the value an environment setting in each deployment environment, not an accidental value inherited from a laptop.

Use the AWS credential chain safely

Prefer workload roles (for example, a task or instance role) in AWS-hosted environments. For local development, use the standard AWS configuration or environment variables. Never put access keys in a PDF, a repository, a client-side bundle or a URL. Grant only the bucket and key-prefix permissions the worker needs.

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

Understand checksum behavior

AWS documents default CRC32 upload checksum calculation for AWS SDK for JavaScript v3.729.0 and later when no precalculated checksum or alternate algorithm is selected. This is version- and configuration-dependent. Pin and review your SDK version, and confirm the effective checksum settings before relying on that behavior as an integrity control.

Make completion and retries reliable

  • Call doc.end() exactly once. Calling it too early truncates the document; never calling it leaves the upload waiting.
  • Attach handlers for both PDF-generation and S3 errors. A PDF stream error must fail the job, not merely print a warning.
  • Await s3.send(...) or upload.done() before acknowledging a queue message, returning success to an API caller or deleting source data.
  • Use deterministic, collision-resistant keys. If a retry must not overwrite an existing report, include a report ID or UUID and enforce that policy in your application.
  • Retry transient AWS failures with bounded backoff, but do not blindly retry credential, permission, validation or malformed-key errors.
  • For multipart jobs, make sure aborted uploads are cleaned up and that the worker can resume or safely create a new key according to your idempotency design.

Troubleshooting common failures

AccessDenied

The identity lacks permission, a bucket policy denies the request, or encryption requirements are not satisfied. Check the effective role, bucket policy, key prefix and any required server-side-encryption headers. Do not solve this by making the object public.

NoSuchBucket or a Region redirect

Confirm the bucket name exactly and set AWS_REGION to the bucket’s Region. Avoid silently falling back to a local profile’s Region.

The object is empty or unreadable

Ensure PDFKit received all content before doc.end(), collect all buffer chunks until the end event, and set ContentType. In a stream pipeline, verify that the producer’s error reaches the upload promise.

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

The process runs out of memory

Stop buffering the entire PDF. Stage to a temporary file or use a tested multipart stream. Also check whether multiple reports are being generated concurrently and cap worker concurrency.

The upload hangs

A stream may never have been finalized, or an error may not be connected to the upload. Call doc.end(), attach error handlers before starting generation, and inspect whether the upload promise is actually awaited.

EntityTooLarge or a size-limit error

Use the upload method appropriate to the output size, such as the SDK v3 multipart helper, and verify the current service and operation limits for your selected method. Do not rely on a limit shown in an old sample without checking its applicability.

Retries create duplicate reports

Use an application-level report ID in the key, record job state separately, and decide whether a retry should overwrite, verify, or create a new object. S3 success alone does not tell your application whether a prior worker attempt also completed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If the PDF is being generated from a web page rather than PDFKit, ScreenshotNeo can return a PDF from one request without making you build browser automation. 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, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.

Generate the PDF with a single call, then pass the response bytes to the same S3 PutObjectCommand shown above:

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

See the ScreenshotNeo documentation for PDF parameters and authentication. Free usage includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Operational checklist

  • PDF generation completed and the stream was ended.
  • Bucket, key and Region were validated from configuration.
  • Credentials use a least-privilege identity.
  • ContentType is application/pdf.
  • The selected buffer, temporary-file or multipart path matches the document size.
  • Generation and upload errors are observable without logging secrets or document contents.
  • The job reports success only after the upload promise resolves.
  • Access to the resulting object follows your application’s security policy.

Frequently Asked Questions

Can I upload a PDF stream directly to S3?

Yes. PDFKit exposes a readable Node.js stream, and AWS SDK v3’s multipart helper can consume a stream, but test error propagation, finalization and backpressure with your installed versions.

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

What is the simplest approach for a small PDF?

Collect PDFKit chunks into a Buffer and send it as Body in PutObjectCommand with ContentType set to application/pdf.

Will S3 automatically make my PDF downloadable?

S3 stores the object; retrieval and download behavior depend on your bucket policy, object permissions and response headers. Keep access private unless your application explicitly requires otherwise.

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.