Free tools Windows power users keep installed
One-click scans. No signup required.
Generate the PDF, finalize it with doc.end(), and upload the resulting bytes or stream with AWS SDK for JavaScript v3. Use PutObjectCommand for modest documents, set the bucket’s actual Region, provide working AWS credentials, and wait for the upload promise before reporting success. For larger files, use the SDK v3 multipart helper, @aws-sdk/lib-storage.
What you need before writing code
- An Active LTS release of Node.js.
- An AWS account, an S3 bucket, and an IAM identity allowed to write objects to that bucket.
- The bucket Region, configured explicitly in deployment settings.
- Credentials supplied through the AWS SDK’s normal credential provider chain (environment variables, shared configuration, workload identity, or an attached role). Do not put access keys in source code.
Install the libraries in a new project:
npm init -y
npm install pdfkit @aws-sdk/client-s3
If your project uses ECMAScript modules, add "type": "module" to package.json, or use the equivalent require syntax for CommonJS.
Option 1: Generate in memory, then use PutObject
This is the simplest complete implementation for a PDF whose size is reasonable for your process memory. PDFKit’s PDFDocument is a readable Node.js stream; it does not write a file by itself. Collect its chunks, call doc.end() to finalize the document, then send the resulting Buffer to S3.
import PDFDocument from "pdfkit";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
const region = process.env.AWS_REGION;
const bucket = process.env.PDF_BUCKET;
if (!region || !bucket) {
throw new Error("AWS_REGION and PDF_BUCKET are required");
}
function makePdfBuffer() {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({
size: "A4",
margin: 50,
info: { Title: "S3 report" }
});
const chunks = [];
doc.on("data", chunk => chunks.push(chunk));
doc.once("end", () => resolve(Buffer.concat(chunks)));
doc.once("error", reject);
doc.fontSize(20).text("S3 report", { align: "center" });
doc.moveDown();
doc.fontSize(12).text(`Created: ${new Date().toISOString()}`);
doc.text("This PDF was generated by Node.js and uploaded to Amazon S3.");
doc.end();
});
}
const s3 = new S3Client({ region });
try {
const pdfBuffer = await makePdfBuffer();
const key = `reports/${crypto.randomUUID()}.pdf`;
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("PDF upload failed", {
name: error.name,
message: error.message,
httpStatusCode: error.$metadata?.httpStatusCode,
requestId: error.$metadata?.requestId
});
process.exitCode = 1;
}
The ContentType metadata makes browsers and downstream consumers treat the object as a PDF. Choose a deliberate key scheme, such as a tenant and report identifier, and apply the bucket’s normal encryption, retention, and access policy. Do not make the bucket public merely to make a generated document retrievable.
Why doc.end() matters
PDFKit emits bytes while the document is being built. Calling doc.end() writes the final PDF structures and allows the end event to fire. If it is omitted, your promise can remain pending or the uploaded bytes can be incomplete.
Can you send a PDF stream directly to S3?
Yes, but treat stream composition as an implementation that needs testing rather than assuming that any two Node streams are interchangeable. You must finalize the PDF, propagate producer and upload errors, respect backpressure, and wait for the upload promise.
For a practical staged approach, generate the PDF to a temporary file, open that file with fs.createReadStream(), and pass the stream to an upload operation. This keeps the complete PDF out of memory while using a well-defined readable stream. Ensure temporary files are deleted in a finally block, including when S3 rejects the request.
Option 2: Multipart upload for larger PDFs
A large or unpredictable document is a better candidate for the SDK v3 multipart helper. Install it:
Recommended Free Tools
npm install @aws-sdk/lib-storage
The helper accepts a readable body and manages multipart parts and retries. The following example writes PDFKit output to a temporary file first, then uploads that file without buffering the entire document in a single JavaScript Buffer:
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { randomUUID } from "node:crypto";
import PDFDocument from "pdfkit";
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
const file = path.join(os.tmpdir(), `report-${randomUUID()}.pdf`);
const region = process.env.AWS_REGION;
const bucket = process.env.PDF_BUCKET;
function writePdf(filename) {
return new Promise((resolve, reject) => {
const doc = new PDFDocument();
const output = fs.createWriteStream(filename);
const fail = error => reject(error);
doc.once("error", fail);
output.once("error", fail);
output.once("finish", resolve);
doc.pipe(output);
doc.fontSize(18).text("Large S3 report");
doc.text("Generated as a stream and staged on disk.");
doc.end();
});
}
try {
await writePdf(file);
const upload = new Upload({
client: new S3Client({ region }),
params: {
Bucket: bucket,
Key: `reports/${randomUUID()}.pdf`,
Body: fs.createReadStream(file),
ContentType: "application/pdf"
}
});
upload.on("httpUploadProgress", progress => {
console.log({ loaded: progress.loaded, total: progress.total });
});
const result = await upload.done();
console.log({ etag: result.ETag });
} finally {
await fs.promises.rm(file, { force: true });
}
A direct PDFKit-to-multipart pipeline can remove disk staging, but verify the exact installed versions and failure behavior. Confirm that the SDK accepts the stream, the producer is ended, errors from both sides reject the operation, and backpressure does not cause unbounded memory growth.
Choosing the transfer method
| Method | Memory | Disk | Best fit | Main risk |
|---|---|---|---|---|
Buffer + PutObjectCommand |
Entire PDF in memory | None | Small, bounded reports | Memory pressure and one request retrying the whole body |
| Temporary file + readable stream | Small application buffer | Uses temporary storage | Large files when operational simplicity matters | Cleanup, disk capacity, and orphaned files |
Multipart Upload |
Part-sized buffers | Optional | Large or variable output | More moving parts and stream error handling |
These are engineering trade-offs, not a published performance benchmark. Select based on memory limits, available temporary storage, expected object size, retry requirements, and how much complexity your service can operate.
Credentials, Region, and integrity
Use the bucket’s real Region
Construct S3Client with the Region where the bucket exists. A wrong or accidentally inherited Region can produce redirects, authorization failures, or confusing endpoint errors. Set AWS_REGION in each deployment environment rather than relying on a developer workstation’s defaults.
Rank #3
Grant only the required permission
The runtime identity generally needs s3:PutObject for the target bucket and prefix. Add read, delete, or tagging permissions only when the application actually uses them. Keep credentials outside logs and source control.
Checksums are version-dependent
AWS documents default CRC32 upload checksum calculation for AWS SDK for JavaScript v3 beginning with version 3.729.0 when no precomputed checksum or other algorithm is selected. Verify your installed SDK version and configuration before depending on that behavior; it is not a universal guarantee for every v3 release.
Retrieving the uploaded PDF safely
An S3 object is not automatically a public URL. Prefer private objects and grant access through your application or a short-lived presigned URL. Store the bucket and key as your durable reference. If you must expose a browser download, set download-oriented response headers when creating the presigned request rather than changing the bucket to public.
Troubleshooting common failures
AccessDenied
Check the runtime role, bucket policy, object-prefix condition, account boundary, and encryption-key permissions. Confirm that the request is targeting the intended account and Region.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →NoSuchBucket or redirect errors
Verify the bucket name exactly and set AWS_REGION to the bucket’s Region. Do not infer the Region from a local profile that differs from production.
The uploaded file will not open
Make sure doc.end() is called, wait for the PDF stream’s completion before uploading, and do not convert binary chunks to UTF-8 strings. For a staged file, wait for the write stream’s finish event before opening it for upload.
The process runs out of memory
Replace the buffer approach with temporary-file streaming or multipart upload. Also check that you are not retaining chunk arrays or creating multiple PDFs concurrently without a limit.
The request hangs
Attach error listeners to both PDF generation and destination streams, ensure the document is finalized, and await the upload completion promise. Add an application timeout and log request metadata, not document contents or secrets.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsBest Value
Multipart uploads remain incomplete
Always await upload.done(). On cancellation or a fatal failure, configure cleanup for abandoned multipart uploads through your S3 lifecycle policy and investigate the original stream error.
Or skip the browser setup
If the PDF is one step in a web-capture workflow, ScreenshotNeo can return a clean screenshot or PDF from one API request. It accepts cookie and 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, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Example cURL request (see the ScreenshotNeo documentation for options):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. After receiving the response, upload shot.webp or a PDF response to S3 using the same PutObjectCommand pattern, with the matching content type. Create a free ScreenshotNeo account to get the 1,000 monthly shots.
Operational checklist
- Generate a unique, intentional key and validate tenant or user input used in that key.
- Set
ContentTypetoapplication/pdffor PDF objects. - Await generation completion and the S3 upload promise.
- Record bucket, key, status, request ID, and size; never log credentials or PDF contents.
- Keep objects private unless a documented security requirement says otherwise.
- Test retries, cancellation, large documents, malformed input, and concurrent generation under your production limits.
Frequently Asked Questions
Do I have to save the PDF to disk before uploading it?
No. A buffered PutObjectCommand upload can send a Buffer directly. Disk staging is one way to reduce memory use, not a requirement.
What does a successful S3 upload return?
The SDK resolves with response metadata such as an ETag and request metadata. Treat the object as uploaded only after that promise resolves.
Should I use a public S3 URL for generated reports?
Usually not. Keep the object private and provide access through your application or a short-lived presigned URL.
Quick Recap
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.

