Run PDFluent on Lambda, Docker, WASM, and in parallel

This guide shows developers how to deploy the PDFluent SDK for four specific tasks: serverless functions, containerised services, browser-based apps, and high-volume batch processing.

Run PDF processing on AWS Lambda with Rust

PDFluent has no native C/C++ dependencies and no JVM, which suits it to Lambda. Cold starts are short for a typical PDF handler.
rust
use lambda_runtime::{service_fn, LambdaEvent, Error};
use aws_sdk_s3::Client as S3Client;
use pdfluent::PdfDocument;
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Error> {
    lambda_runtime::run(service_fn(handler)).await
}

async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
    let bucket = event.payload["bucket"].as_str().unwrap_or_default();
    let key    = event.payload["key"].as_str().unwrap_or_default();

    let config = aws_config::load_from_env().await;
    let s3 = S3Client::new(&config);

    let resp = s3.get_object().bucket(bucket).key(key).send().await?;
    let bytes = resp.body.collect().await?.into_bytes();

    let doc = PdfDocument::from_bytes(&bytes)?;
    let page_count = doc.page_count();
    let text = doc.text()?;

    Ok(serde_json::json!({
        "pages": page_count,
        "chars": text.len()
    }))
}
  1. Step 1

    Set up your Cargo project for Lambda

    Use the lambda_runtime crate from AWS. Build a binary named bootstrap, which is the required name for Lambda custom runtimes.

    rust
    # Cargo.toml
    [package]
    name = "pdf-lambda"
    version = "0.1.0"
    edition = "2021"
    
    [[bin]]
    name = "bootstrap"
    path = "src/main.rs"
    
    [dependencies]
    pdfluent = "1.1.0"
    lambda_runtime = "0.11"
    aws-config = { version = "1", features = ["behavior-version-latest"] }
    aws-sdk-s3 = "1"
    tokio = { version = "1", features = ["full"] }
    serde_json = "1"
  2. Step 2

    Cross-compile for Amazon Linux

    Lambda runs on Amazon Linux 2 (x86_64 or arm64). Use cargo-lambda to cross-compile without a Linux machine.

    rust
    # Install cargo-lambda
    cargo install cargo-lambda
    
    # Build for x86_64 Lambda
    cargo lambda build --release --target x86_64-unknown-linux-musl
    
    # Or build for arm64 Lambda (Graviton2, cheaper)
    cargo lambda build --release --target aarch64-unknown-linux-musl
  3. Step 3

    Write the Lambda handler

    Read the S3 bucket and key from the event payload. Download the PDF bytes from S3 and pass them to PdfDocument::from_bytes.

    rust
    async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
        let bucket = event.payload["bucket"].as_str().unwrap_or_default();
        let key    = event.payload["key"].as_str().unwrap_or_default();
    
        let config = aws_config::load_from_env().await;
        let s3 = S3Client::new(&config);
    
        let resp = s3.get_object()
            .bucket(bucket)
            .key(key)
            .send()
            .await?;
    
        let bytes = resp.body.collect().await?.into_bytes();
        let doc = PdfDocument::from_bytes(&bytes)?;
        // ... process the document
        Ok(serde_json::json!({ "pages": doc.page_count() }))
    }
  4. Step 4

    Deploy with cargo-lambda

    cargo lambda deploy uploads the binary as a Lambda function with the provided.al2 runtime.

    rust
    cargo lambda deploy \
      --region eu-west-1 \
      --memory 512 \
      --timeout 30 \
      pdf-lambda
  5. Step 5

    Set the Lambda execution role

    The function needs GetObject permission on the S3 bucket. Attach an inline policy or a managed policy to the execution role.

    rust
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["s3:GetObject"],
          "Resource": "arn:aws:s3:::my-pdf-bucket/*"
        }
      ]
    }
  • PDFluent links statically via musl. The final bootstrap binary stays within the Lambda zipped package limit.
  • Set Lambda memory according to your document sizes, and check the provider's current documentation for the limits. Larger PDFs need more memory than small ones.
  • Lambda /tmp size and configuration change over time, so check the provider's current documentation. Write temporary output there if you need to save before uploading to S3.
  • A musl-linked Rust binary is one static file with no JVM or Python interpreter to start.
Does PDFluent need any Linux system libraries?
No. PDFluent is pure Rust with no C library dependencies beyond libc, which musl provides statically. There is no need to install system packages in the Lambda environment.
How do I handle PDFs larger than Lambda memory?
For PDFs larger than your Lambda memory allocation, increase the memory setting, or switch to a container image Lambda. Alternatively, split large PDFs into page ranges before processing.
Can I write the result back to S3?
Yes. After processing, serialize the output with doc.to_bytes() and call s3.put_object().bucket(bucket).key(out_key).body(bytes.into()).send().await?.
Does this work with Lambda@Edge or CloudFront Functions?
Lambda@Edge supports custom runtimes and works with the same bootstrap binary. CloudFront Functions use a JavaScript-only runtime and cannot run Rust binaries. Use Lambda@Edge for origin-request PDF generation.

Run PDFluent in a Docker container

PDFluent has no native dependencies. The Docker image is small and requires no additional apt packages.
rust
# Dockerfile
FROM rust:1.78-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
COPY --from=builder /app/target/release/pdf-processor /usr/local/bin/
ENTRYPOINT ["pdf-processor"]
  1. Step 1

    Create a minimal Rust project

    Your binary reads PDF bytes from stdin or a file path passed as an argument and writes results to stdout.

    rust
    # Cargo.toml
    [package]
    name = "pdf-processor"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    pdfluent = "1.1.0"
  2. Step 2

    Write a simple PDF processing binary

    Accept the file path as a command-line argument. Process the document and print results to stdout.

    rust
    // src/main.rs
    use pdfluent::PdfDocument;
    use std::env;
    
    fn main() -> pdfluent::Result<()> {
        let path = env::args().nth(1).expect("usage: pdf-processor <file.pdf>");
        let doc = PdfDocument::open(&path)?;
    
        println!("pages: {}", doc.page_count());
        println!("title: {:?}", doc.metadata().title);
        println!("chars: {}", doc.text()?.len());
    
        Ok(())
    }
  3. Step 3

    Write a multi-stage Dockerfile

    The builder stage compiles the binary. The final stage copies only the binary into a minimal debian image. No Rust toolchain ships in the final image.

    rust
    # Dockerfile
    FROM rust:1.78-slim AS builder
    WORKDIR /app
    
    # Cache dependencies first
    COPY Cargo.toml Cargo.lock ./
    RUN mkdir src && echo "fn main(){}" > src/main.rs
    RUN cargo build --release
    RUN rm -f target/release/deps/pdf_processor*
    
    # Build the real binary
    COPY src ./src
    RUN cargo build --release
    
    # Final image
    FROM debian:bookworm-slim
    RUN apt-get update && apt-get install -y --no-install-recommends     ca-certificates && rm -rf /var/lib/apt/lists/*
    
    COPY --from=builder /app/target/release/pdf-processor /usr/local/bin/
    ENTRYPOINT ["pdf-processor"]
  4. Step 4

    Build and test the image locally

    Build the image and run it with a test PDF mounted as a volume.

    rust
    docker build -t pdf-processor:latest .
    
    # Run with a local PDF
    docker run --rm   -v "$(pwd)/samples:/data"   pdf-processor:latest   /data/test.pdf
  5. Step 5

    Use a musl target for a fully static binary

    If your final base image is scratch or alpine, build a statically linked musl binary. This eliminates the glibc version dependency.

    rust
    # Dockerfile (scratch variant)
    FROM rust:1.78-slim AS builder
    RUN rustup target add x86_64-unknown-linux-musl
    RUN apt-get update && apt-get install -y musl-tools
    WORKDIR /app
    COPY . .
    RUN cargo build --release --target x86_64-unknown-linux-musl
    
    FROM scratch
    COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/pdf-processor /
    ENTRYPOINT ["/pdf-processor"]
  • The debian:bookworm-slim final image is larger than the scratch variant, which carries the binary only.
  • PDFluent does not call fontconfig, freetype, or any system font libraries. Font data is embedded in the crate.
  • For processing PDFs that require CJK fonts, ensure the font bytes are compiled into the binary or mounted as a volume.
  • Use COPY --chown in the Dockerfile to run the binary as a non-root user in production.
Does PDFluent need any apt packages in the container?
No. PDFluent is pure Rust. The only apt package in the example Dockerfile is ca-certificates, which is needed if your binary makes outbound HTTPS calls. If you only process local files, even that is optional.
How do I pass a PDF into the container at runtime?
Mount a host directory with -v /host/path:/data and pass /data/file.pdf as the argument. Alternatively, pipe PDF bytes to stdin and have your binary read from std::io::stdin().
Can I run this on AWS ECS or Google Cloud Run?
Yes. Both accept Docker images. Push the image to ECR or Artifact Registry, then create a task or service. Cloud Run is especially convenient because it scales to zero automatically.
How do I reduce build time in CI?
Use the layer caching trick in step 3: copy Cargo.toml first, compile a dummy main.rs, then copy the real source. This caches dependency compilation as long as Cargo.toml does not change.

Compile PDFluent to WASM and use it in the browser

Run PDF processing entirely client-side. No server round-trip, no file upload, no privacy risk.
rust
// src/lib.rs
use wasm_bindgen::prelude::*;
use pdfluent::PdfDocument;

#[wasm_bindgen]
pub fn page_count(bytes: &[u8]) -> Result<u32, JsValue> {
    let doc = PdfDocument::from_bytes(bytes)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    Ok(doc.page_count())
}

#[wasm_bindgen]
pub fn extract_text(bytes: &[u8]) -> Result<String, JsValue> {
    let doc = PdfDocument::from_bytes(bytes)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    doc.text()
        .map_err(|e| JsValue::from_str(&e.to_string()))
}
  1. Step 1

    Add the wasm feature and wasm-bindgen

    PDFluent has a wasm feature that disables std-only dependencies. wasm-bindgen generates the JS glue code.

    rust
    # Cargo.toml
    [package]
    name = "pdf-wasm"
    version = "0.1.0"
    edition = "2021"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    pdfluent = { version = "0.9", features = ["wasm"] }
    wasm-bindgen = "0.2"
  2. Step 2

    Expose Rust functions to JavaScript

    Mark each function with #[wasm_bindgen]. Functions that receive PDF bytes take &[u8]. Return JsValue errors so JavaScript can catch them.

    rust
    use wasm_bindgen::prelude::*;
    use pdfluent::PdfDocument;
    
    #[wasm_bindgen]
    pub fn page_count(bytes: &[u8]) -> Result<u32, JsValue> {
        let doc = PdfDocument::from_bytes(bytes)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        Ok(doc.page_count())
    }
  3. Step 3

    Build the WASM module

    Install wasm-pack and run the build command. This produces a pkg/ directory with .wasm and JS binding files.

    rust
    # Install wasm-pack
    cargo install wasm-pack
    
    # Build for the browser (ES module output)
    wasm-pack build --target web --out-dir pkg
    
    # Or build for Node.js
    wasm-pack build --target nodejs --out-dir pkg
  4. Step 4

    Import the module in JavaScript

    Load the WASM module asynchronously, then pass ArrayBuffer data from a file input or fetch.

    rust
    import init, { page_count, extract_text } from './pkg/pdf_wasm.js';
    
    await init();
    
    const input = document.getElementById('file-input');
    input.addEventListener('change', async (e) => {
        const file = e.target.files[0];
        const buffer = await file.arrayBuffer();
        const bytes = new Uint8Array(buffer);
    
        const pages = page_count(bytes);
        const text  = extract_text(bytes);
    
        console.log(`Pages: ${pages}`);
        console.log(`Text: ${text.slice(0, 200)}`);
    });
  5. Step 5

    Serve with correct MIME type

    Browsers require .wasm files to be served with Content-Type: application/wasm. Most dev servers handle this automatically. In production, verify your CDN or server config.

    rust
    # Vite, webpack, and Parcel all handle WASM automatically.
    # For a plain nginx config:
    # types {
    #     application/wasm wasm;
    # }
    
    # Quick local test with wasm-pack's built-in server:
    npx serve .
  • The published @pdfluent/sdk-wasm@1.0.0 .wasm is 15.3 MB unpacked, 3.8 MB Brotli, 5.5 MB gzip [B07, 2026-09-16]. Use wasm-opt from binaryen to reduce it.
  • WASM runs in a single thread by default. For parallel operations in the browser, use Web Workers and pass PDF bytes via postMessage.
  • PDFluent in WASM mode disables features that require OS calls, such as file I/O and system fonts. All input must come through the &[u8] parameter.
  • SharedArrayBuffer (required for wasm-bindgen threads) needs the COOP and COEP headers. If you do not need threads, standard ArrayBuffer works everywhere.
Does PDFluent WASM work in React and Vue apps?
Yes. Import the pkg/ directory from wasm-pack into any JavaScript bundler. Vite and webpack both handle WASM imports well. Initialize once with await init() before calling any exported function.
Can I use PDFluent WASM in a Web Worker?
Yes, and it is the recommended pattern for heavy operations. Create a Worker, pass the PDF bytes via postMessage with a Transferable, call the WASM functions inside the worker, and post the result back to the main thread.
What is the browser memory limit for WASM?
WASM memory starts small and grows on demand. Browsers set their own limits, so check current documentation. Processing a large PDF uses more WASM heap than a small one.
Can I generate and download a PDF in the browser with WASM?
Yes. Return Vec<u8> from a #[wasm_bindgen] function, convert it to a Uint8Array in JavaScript, create a Blob with type application/pdf, and use URL.createObjectURL to trigger a download.

Process thousands of PDFs in parallel with Rust

Use rayon and PDFluent together to process a folder of PDFs across all CPU cores. No thread pool setup required.
rust
use rayon::prelude::*;
use std::fs;
use pdfluent::PdfDocument;

fn main() -> anyhow::Result<()> {
    let files: Vec<_> = fs::read_dir("./input")?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.extension().map_or(false, |e| e == "pdf"))
        .collect();

    files.par_iter().for_each(|path| {
        match process_file(path) {
            Ok(_) => println!("OK  {:?}", path.file_name().unwrap()),
            Err(e) => eprintln!("ERR {:?}: {}", path.file_name().unwrap(), e),
        }
    });

    Ok(())
}

fn process_file(path: &std::path::Path) -> pdfluent::Result<()> {
    let doc = PdfDocument::open(path)?;
    let text = doc.text()?;
    let out = path.with_extension("txt");
    fs::write(out, text)?;
    Ok(())
}
  1. Step 1

    Add PDFluent and rayon to Cargo.toml

    rayon provides a parallel iterator that distributes work across all available CPU cores automatically.

    rust
    # Cargo.toml
    [dependencies]
    pdfluent = "1.1.0"
    rayon = "1.10"
    anyhow = "1"
  2. Step 2

    Collect the list of PDF files

    Use std::fs::read_dir to walk the input directory. Filter by extension to skip non-PDF files.

    rust
    use std::fs;
    use std::path::PathBuf;
    
    let files: Vec<PathBuf> = fs::read_dir("./input")?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.extension().map_or(false, |ext| ext == "pdf"))
        .collect();
    
    println!("Found {} PDF files", files.len());
  3. Step 3

    Process files in parallel with rayon

    Replace .iter() with .par_iter() to run each file on a separate thread. PdfDocument::open is Send, so it works safely across rayon threads.

    rust
    use rayon::prelude::*;
    
    files.par_iter().for_each(|path| {
        match process_file(path) {
            Ok(_) => println!("OK  {}", path.display()),
            Err(e) => eprintln!("ERR {}: {}", path.display(), e),
        }
    });
  4. Step 4

    Write the per-file processing function

    Keep the function focused on one document. Open, process, save. Errors are returned and logged by the caller.

    rust
    use pdfluent::{PdfDocument, WatermarkOptions};
    
    fn process_file(path: &std::path::Path) -> pdfluent::Result<()> {
        let mut doc = PdfDocument::open(path)?;
    
        // Example: stamp a watermark across the document
        doc.add_watermark("CONFIDENTIAL", WatermarkOptions::centered())?;
    
        let out_path = std::path::Path::new("./output")
            .join(path.file_name().unwrap());
        doc.save(out_path)?;
        Ok(())
    }
  5. Step 5

    Collect results for a summary report

    Use par_iter().map() instead of par_iter().for_each() when you need to collect success and error counts.

    rust
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;
    
    let ok_count = Arc::new(AtomicUsize::new(0));
    let err_count = Arc::new(AtomicUsize::new(0));
    
    files.par_iter().for_each(|path| {
        let ok = ok_count.clone();
        let err = err_count.clone();
        match process_file(path) {
            Ok(_) => { ok.fetch_add(1, Ordering::Relaxed); }
            Err(e) => {
                eprintln!("ERR {}: {}", path.display(), e);
                err.fetch_add(1, Ordering::Relaxed);
            }
        }
    });
    
    println!(
        "Done. {} ok, {} errors",
        ok_count.load(Ordering::Relaxed),
        err_count.load(Ordering::Relaxed)
    );
  • rayon uses one thread per logical CPU by default. Set RAYON_NUM_THREADS=4 to cap concurrency, which is useful on memory-constrained servers.
  • PdfDocument::open reads the full document into memory. For very large files, set RAYON_NUM_THREADS equal to the number of files you can fit in RAM simultaneously.
  • Output directory must exist before you start. Create it with fs::create_dir_all("./output")? near the top of main().
  • For recursive directory traversal use the walkdir crate alongside rayon::iter::ParallelBridge.
How fast is parallel PDF processing with rayon?
Gains depend on per-file work: text extraction is CPU-bound and scales well; saving to a single spinning disk does not.
What if some files are password-protected?
PdfDocument::open returns Err for password-protected files. Handle the error in your process_file function, either by skipping the file or by retrying with PdfDocument::open_with.
Can I process PDFs streamed from S3 instead of the local filesystem?
Yes. Use PdfDocument::from_bytes(bytes) where bytes is the content downloaded from S3. The AWS SDK for Rust returns Vec<u8> from get_object, which you pass directly.
How do I limit memory usage during batch processing?
Use rayon::ThreadPoolBuilder::new().num_threads(n).build_global() to cap the number of simultaneous documents in memory. Pick n from how many typical documents fit in your available RAM.