Use AWS SDK for Go v2 and send the generated document as an io.Reader in an S3 PutObject request. For a PDF held in memory, wrap its bytes with bytes.NewReader, provide the destination bucket and key, and set ContentType to application/pdf. For large documents or high-throughput workloads, use the SDK transfer manager for bounded multipart uploads.
The upload boundary: PDF bytes to an S3 object
Keep PDF generation and object storage as separate concerns. Your chosen generator can return []byte, write to an io.Writer, or expose a seekable stream; the storage function only needs a body that the AWS SDK can read. This avoids coupling S3 code to a particular PDF package.
The minimum destination inputs are:
- Bucket: the S3 bucket that receives the object.
- Key: the complete object name, including any prefix such as
reports/2026/09/invoice-123.pdf. - Body: the PDF data as an
io.Reader.
Set the MIME metadata explicitly. S3’s Content-Type describes the representation stored in the object, so a PDF should be uploaded with application/pdf. If browsers or download clients need a particular filename, add Content-Disposition as well.
Prerequisites and AWS client setup
- Go and the AWS SDK for Go v2 in your module.
- An AWS identity permitted to upload to the target bucket (at minimum, an appropriate
s3:PutObjectpermission). - A bucket name, region, and an object-key policy that matches your overwrite and retention requirements.
Use the SDK’s default credential and region chain rather than embedding secrets in source code. In production, that normally means an IAM role, workload identity, or another managed provider. The exact provider depends on where your Go service runs.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →#1 Best Overall
go mod init example.com/pdf-uploader
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
The client can then be created with the default configuration:
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
if err != nil {
return fmt.Errorf("load AWS configuration: %w", err)
}
client := s3.NewFromConfig(cfg)
Upload a generated PDF held in memory
This is the direct path for modest documents. The generator produces bytes, and bytes.NewReader adapts those bytes to the SDK’s reader-based body field.
package storage
import (
"bytes"
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// SavePDF uploads one complete PDF object to Amazon S3.
func SavePDF(ctx context.Context, client *s3.Client, bucket, key string, pdf []byte) error {
if len(pdf) == 0 {
return fmt.Errorf("refusing to upload an empty PDF")
}
if bucket == "" || key == "" {
return fmt.Errorf("bucket and key are required")
}
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(pdf),
ContentType: aws.String("application/pdf"),
// ContentDisposition: aws.String(`attachment; filename="report.pdf"`),
})
if err != nil {
return fmt.Errorf("put PDF %s/%s: %w", bucket, key, err)
}
return nil
}
Call the function only after generation succeeds:
pdfBytes, err := buildReport(ctx, report)
if err != nil {
return fmt.Errorf("generate report: %w", err)
}
key := fmt.Sprintf("reports/%s.pdf", report.ID)
if err := SavePDF(ctx, client, os.Getenv("PDF_BUCKET"), key, pdfBytes); err != nil {
return err
}
Do not announce a successful save until PutObject returns without an error. The returned error is the upload result; preserve it so callers, metrics, and retry logic can distinguish generation failures from storage failures.
Stream or upload a file without coupling to a generator
If a generator writes to disk, open the file and pass the file handle as the body. This avoids loading the entire document into a second byte slice.
func UploadPDFFile(ctx context.Context, client *s3.Client, bucket, key, path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open PDF: %w", err)
}
defer f.Close()
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: f,
ContentType: aws.String("application/pdf"),
})
if err != nil {
return fmt.Errorf("upload PDF: %w", err)
}
return nil
}
The same boundary works for a generator that writes to an io.Pipe, but coordinate errors from both the producer and consumer carefully. If the producer fails after the upload starts, close the pipe with that error so the S3 operation does not appear to have completed normally.
Choosing and validating object keys
A key is not a local path; it is the exact object identifier within the bucket. Decide whether a repeated key should overwrite an earlier report or whether every output needs a unique key.
| Requirement | Key strategy | Operational consequence |
|---|---|---|
| Latest report only | reports/current.pdf |
New uploads replace the prior object. |
| Immutable report history | reports/{reportID}/{version}.pdf |
Each version remains addressable; cleanup must be planned. |
| Collision resistance for retries | Include a stable report ID and attempt or version | Retries can be made idempotent without accidentally replacing another report. |
Normalize values that enter keys, reject empty IDs, and avoid allowing an untrusted request to select arbitrary prefixes. Bucket versioning can provide an additional recovery layer, but it does not replace a deliberate key policy.
When multipart transfer is the better fit
Direct PutObject is the simplest choice for ordinary PDF outputs. For large PDFs or many concurrent uploads, the AWS SDK for Go v2 transfer manager can split the body into parts and upload those parts concurrently.
Recommended Free Tools
| Axis | Direct PutObject |
Transfer manager |
|---|---|---|
| Input | Reader or file passed directly to one operation. | Body supplied to a manager that can divide it into parts. |
| Complexity | Low. | Requires part-size and concurrency decisions. |
| Throughput | One request path. | Concurrent multipart parts can improve transfer throughput. |
| Resource control | Few parallel-transfer knobs. | Bound concurrent uploads and account for memory and network use. |
AWS documents a 5 MiB minimum part size for multipart uploads and warns that applications should limit concurrent calls to avoid resource exhaustion. There is no universal safe concurrency number: choose one using your service’s memory, bandwidth, request rate, and workload, then measure it in your environment.
Use multipart for a known large-output class rather than enabling it reflexively for every PDF. A small document gains complexity without a meaningful benefit.
Metadata, completion, and verification
Content type and download behavior
ContentType: aws.String("application/pdf") lets clients handle the object as a PDF. Add ContentDisposition when the response should download with a controlled name, for example attachment; filename="invoice-123.pdf". Treat filenames derived from user input as untrusted and quote or sanitize them.
Wait only when your workflow needs confirmation
S3 upload completion is normally represented by a successful SDK response. If a subsequent workflow must verify that the object can be found, use an S3 object-exists waiter after the upload. Do not add a waiter to every request merely as a substitute for checking the original error; it adds latency and another API call.
Free tools Windows power users keep installed
One-click scans. No signup required.
Cancellation and retries
Pass a request-scoped context with a deadline. Cancellation should stop work when the caller disappears. Retry transient failures according to the SDK’s behavior and your service’s idempotency policy; a deterministic key makes a retry safe when replacing the same logical report is intended.
Performance and reliability checklist
- Generate in memory when the PDF is modest and the extra byte allocation is acceptable.
- Use a file or streaming reader when buffering a large document would pressure heap memory.
- For multipart transfer, bound concurrent parts and select a part size that fits available memory.
- Reuse the S3 client; do not construct a new client for every PDF.
- Attach deadlines and return wrapped errors with bucket and key context.
- Use stable keys for idempotent retries, or unique keys when every attempt must remain distinct.
- Record object size, elapsed time, and failure category in application telemetry.
- Keep credentials out of logs, source code, and generated PDF metadata.
Troubleshooting common failures
AccessDenied
The runtime identity or bucket policy does not allow the operation, or the key is outside an allowed prefix. Confirm the effective IAM policy, bucket policy, account, and region. The error is authorization-related; changing the PDF bytes will not fix it.
NoSuchBucket or wrong region
Check the bucket spelling and the region used to load the SDK configuration. A region mismatch can also surface as redirect or signing errors. Configure the client for the bucket’s actual region.
Rank #4
Signature or credential errors
Verify that the process can obtain credentials from its configured provider and that the system clock is reasonably accurate. Do not paste secret keys into source or error reports.
Empty or corrupt object
Check the generator result before calling S3, ensure the reader has not already been consumed, and do not reuse a reader whose cursor is at the end. For a file, open it before upload and keep it open until the SDK call returns.
Out-of-memory or slow large uploads
Stop converting large output to multiple byte slices. Prefer a file-backed or streaming body, or use the transfer manager with bounded concurrency and an appropriate part size.
Success reported too early
Return the SDK error to the caller and mark the job successful only after the call completes. If another system immediately needs to locate the object, add the targeted existence check described above.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If the PDF is produced from a web page rather than a Go PDF library, ScreenshotNeo can return a clean PDF through one HTTP 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, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides capture_pdf, take_screenshot, and get_page_info tools 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 report.pdf
See the ScreenshotNeo documentation for request options. You can then pass report.pdf to the file-upload function above. ScreenshotNeo includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Best Value
FAQ
Can I upload a PDF with only an io.Reader?
Yes. The SDK’s PutObjectInput.Body is an io.Reader, so bytes, files, pipes, and other reader implementations can be used.
Should every PDF use multipart upload?
No. Use direct PutObject for ordinary outputs; reserve multipart transfer for files or workloads where its concurrency and part-management benefits justify the added operational choices.
Does setting ContentType change the PDF itself?
No. It sets object metadata that tells clients how to interpret the already-generated bytes.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →What PDF generator should a Go project choose?
The title does not establish a best library. Choose based on layout needs, fonts, forms, licensing, and whether the generator can produce the reader or byte representation your storage boundary accepts.
Frequently Asked Questions
Can an upload function accept both generated bytes and files?
Yes. Define the boundary around an io.Reader, then adapt []byte with bytes.NewReader or pass an open *os.File.
When is an S3 waiter useful?
Use an object-exists waiter only when a following workflow needs an explicit lookup confirmation; a successful PutObject response is otherwise the normal completion signal.
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.




