[{"data":1,"prerenderedAt":1116},["Reactive",2],{"/guides/puppeteer-pdf-in-production":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"dateModified":10,"author":11,"body":12,"_type":1111,"_id":1112,"_source":1113,"_file":1114,"_extension":1115},"/guides/puppeteer-pdf-in-production","guides",false,"","How to run Puppeteer page.pdf() in production","Puppeteer page.pdf() in production: production-safe code, our measured numbers on memory, browser reuse and waitUntil, and a container checklist.","2026-11-04","Cyril Nicodème",{"type":13,"children":14,"toc":1101},"root",[15,56,68,75,91,103,109,114,141,326,338,350,356,361,374,393,455,467,473,499,504,561,597,628,634,639,826,832,837,1017,1031,1037,1042,1047,1056],{"type":16,"tag":17,"props":18,"children":19},"element","p",{},[20,23,30,32,38,40,46,48,54],{"type":21,"value":22},"text","To run Puppeteer's ",{"type":16,"tag":24,"props":25,"children":27},"code",{"className":26},[],[28],{"type":21,"value":29},"page.pdf()",{"type":21,"value":31}," in production, launch one browser per process and reuse it, open a new page for each document and close it in ",{"type":16,"tag":24,"props":33,"children":35},{"className":34},[],[36],{"type":21,"value":37},"finally",{"type":21,"value":39},", wait for ",{"type":16,"tag":24,"props":41,"children":43},{"className":42},[],[44],{"type":21,"value":45},"load",{"type":21,"value":47}," and then ",{"type":16,"tag":24,"props":49,"children":51},{"className":50},[],[52],{"type":21,"value":53},"document.fonts.ready",{"type":21,"value":55},", keep a timeout on loading and rendering, and install the fonts your documents use in the image.",{"type":16,"tag":17,"props":57,"children":58},{},[59,61,66],{"type":21,"value":60},"In a tutorial, ",{"type":16,"tag":24,"props":62,"children":64},{"className":63},[],[65],{"type":21,"value":29},{"type":21,"value":67}," is three lines. In production, it's a browser you now operate. We ran the same 30-row invoice through it hundreds of times to put numbers on what goes wrong.",{"type":16,"tag":69,"props":70,"children":72},"h2",{"id":71},"what-does-production-safe-pagepdf-code-look-like",[73],{"type":21,"value":74},"What does production-safe page.pdf() code look like?",{"type":16,"tag":17,"props":76,"children":77},{},[78,80,89],{"type":21,"value":79},"Here's the baseline, written for ",{"type":16,"tag":81,"props":82,"children":86},"a",{"href":83,"rel":84},"https://pptr.dev/supported-browsers",[85],"nofollow",[87],{"type":21,"value":88},"Puppeteer 25.16.0, which ships with Chrome for Testing 156",{"type":21,"value":90},".",{"type":16,"tag":92,"props":93,"children":98},"pre",{"className":94,"code":96,"language":97,"meta":7},[95],"language-javascript","import puppeteer from 'puppeteer';\n\n// One browser per process, launched once and reused for every job.\nconst browser = await puppeteer.launch();\n\nexport async function htmlToPdf(html) {\n    // A new page (tab) per document.\n    const page = await browser.newPage();\n    try {\n        // 'load' waits for the stylesheets and images in the HTML.\n        await page.setContent(html, { waitUntil: 'load', timeout: 30_000 });\n        // Then wait for the web fonts the page asked for.\n        await page.evaluate(() => document.fonts.ready);\n\n        return await page.pdf({\n            format: 'A4',          // the default is Letter\n            printBackground: true, // the default is false: no background colors or images\n            margin: { top: '16mm', right: '16mm', bottom: '16mm', left: '16mm' },\n            timeout: 30_000,\n        });\n    } finally {\n        // Always close the page, even when something above threw.\n        await page.close();\n    }\n}\n","javascript",[99],{"type":16,"tag":24,"props":100,"children":101},{"__ignoreMap":7},[102],{"type":21,"value":96},{"type":16,"tag":69,"props":104,"children":106},{"id":105},"why-does-puppeteers-memory-keep-growing",[107],{"type":21,"value":108},"Why does Puppeteer's memory keep growing?",{"type":16,"tag":17,"props":110,"children":111},{},[112],{"type":21,"value":113},"Because pages you don't close stay open, and each one keeps its own Chromium process and memory long after the PDF is done.",{"type":16,"tag":17,"props":115,"children":116},{},[117,119,125,127,132,134,139],{"type":21,"value":118},"We converted the same invoice 200 times on one browser, once without ",{"type":16,"tag":24,"props":120,"children":122},{"className":121},[],[123],{"type":21,"value":124},"page.close()",{"type":21,"value":126}," and once with it. Without it, the Chromium process tree grew from 323 MB to 2,715 MB after 100 jobs and 5,046 MB after 200, running 206 processes. With ",{"type":16,"tag":24,"props":128,"children":130},{"className":129},[],[131],{"type":21,"value":124},{"type":21,"value":133}," in ",{"type":16,"tag":24,"props":135,"children":137},{"className":136},[],[138],{"type":21,"value":37},{"type":21,"value":140},", it went from 325 MB to 642 MB after 100 jobs and held at 659 MB after 200, with 17 processes. That's about 24 MB for every page you forget.",{"type":16,"tag":142,"props":143,"children":145},"guide-figure",{"caption":144},"Chromium memory over 200 PDF jobs on one browser, with and without page.close(). Our test, Puppeteer 25.16.0, November 4, 2026.",[146],{"type":16,"tag":147,"props":148,"children":157},"svg",{"viewBox":149,"xmlns":150,"role":151,"ariaLabelledBy":152,"style":155,"fontFamily":156},"-34 0 594 300","http://www.w3.org/2000/svg","img",[153,154],"pptr-mem-title","pptr-mem-desc","max-width:594px;min-width:460px;display:block;margin:0 auto","DM Sans, sans-serif",[158,164,170,256,264,269,286,297],{"type":16,"tag":159,"props":160,"children":161},"title",{"id":153},[162],{"type":21,"value":163},"Memory grows by about 24 MB a job when pages aren't closed",{"type":16,"tag":165,"props":166,"children":167},"desc",{"id":154},[168],{"type":21,"value":169},"Line chart of the Chromium process tree's memory in MB after 0, 100 and 200 jobs. Without page.close(): 323, 2,715 and 5,046 MB. With page.close(): 325, 642 and 659 MB.",{"type":16,"tag":171,"props":172,"children":175},"g",{"fontSize":173,"fill":174},"13","#4C556D",[176,184,188,192,196,200,208,214,220,226,232,239,245,250],{"type":16,"tag":177,"props":178,"children":183},"line",{"x1":179,"y1":180,"x2":181,"y2":180,"stroke":182},"70","250","530","#DBE4F2",[],{"type":16,"tag":177,"props":185,"children":187},{"x1":179,"y1":186,"x2":181,"y2":186,"stroke":182},"196",[],{"type":16,"tag":177,"props":189,"children":191},{"x1":179,"y1":190,"x2":181,"y2":190,"stroke":182},"141",[],{"type":16,"tag":177,"props":193,"children":195},{"x1":179,"y1":194,"x2":181,"y2":194,"stroke":182},"87",[],{"type":16,"tag":177,"props":197,"children":199},{"x1":179,"y1":198,"x2":181,"y2":198,"stroke":182},"32",[],{"type":16,"tag":21,"props":201,"children":205},{"x":202,"y":203,"textAnchor":204},"62","254","end",[206],{"type":21,"value":207},"0",{"type":16,"tag":21,"props":209,"children":211},{"x":202,"y":210,"textAnchor":204},"200",[212],{"type":21,"value":213},"1,500",{"type":16,"tag":21,"props":215,"children":217},{"x":202,"y":216,"textAnchor":204},"145",[218],{"type":21,"value":219},"3,000",{"type":16,"tag":21,"props":221,"children":223},{"x":202,"y":222,"textAnchor":204},"91",[224],{"type":21,"value":225},"4,500",{"type":16,"tag":21,"props":227,"children":229},{"x":202,"y":228,"textAnchor":204},"36",[230],{"type":21,"value":231},"6,000 MB",{"type":16,"tag":21,"props":233,"children":237},{"x":234,"y":235,"textAnchor":236},"80","272","middle",[238],{"type":21,"value":207},{"type":16,"tag":21,"props":240,"children":242},{"x":241,"y":235,"textAnchor":236},"300",[243],{"type":21,"value":244},"100",{"type":16,"tag":21,"props":246,"children":248},{"x":247,"y":235,"textAnchor":236},"520",[249],{"type":21,"value":210},{"type":16,"tag":21,"props":251,"children":253},{"x":241,"y":252,"textAnchor":236},"294",[254],{"type":21,"value":255},"Jobs on one browser",{"type":16,"tag":257,"props":258,"children":263},"polyline",{"points":259,"fill":260,"stroke":261,"strokeWidth":262},"80,238 300,151 520,67","none","#FF9D28","3",[],{"type":16,"tag":257,"props":265,"children":268},{"points":266,"fill":260,"stroke":267,"strokeWidth":262},"80,238 300,227 520,226","#6C47FF",[],{"type":16,"tag":171,"props":270,"children":271},{"fill":261},[272,278,282],{"type":16,"tag":273,"props":274,"children":277},"circle",{"cx":234,"cy":275,"r":276},"238","4",[],{"type":16,"tag":273,"props":279,"children":281},{"cx":241,"cy":280,"r":276},"151",[],{"type":16,"tag":273,"props":283,"children":285},{"cx":247,"cy":284,"r":276},"67",[],{"type":16,"tag":171,"props":287,"children":288},{"fill":267},[289,293],{"type":16,"tag":273,"props":290,"children":292},{"cx":241,"cy":291,"r":276},"227",[],{"type":16,"tag":273,"props":294,"children":296},{"cx":247,"cy":295,"r":276},"226",[],{"type":16,"tag":171,"props":298,"children":300},{"fontSize":173,"fontWeight":299},"500",[301,308,316,320],{"type":16,"tag":302,"props":303,"children":307},"rect",{"x":304,"y":305,"width":306,"height":276,"fill":261},"84","46","12",[],{"type":16,"tag":21,"props":309,"children":313},{"x":310,"y":311,"fill":312},"102","52","#0A0C3D",[314],{"type":21,"value":315},"No page.close(): 5,046 MB",{"type":16,"tag":302,"props":317,"children":319},{"x":304,"y":318,"width":306,"height":276,"fill":267},"68",[],{"type":16,"tag":21,"props":321,"children":323},{"x":310,"y":322,"fill":312},"74",[324],{"type":21,"value":325},"With page.close(): 659 MB",{"type":16,"tag":17,"props":327,"children":328},{},[329,331,336],{"type":21,"value":330},"A process manager won't save you here: the browser is healthy, it just has 200 tabs open. Close pages in ",{"type":16,"tag":24,"props":332,"children":334},{"className":333},[],[335],{"type":21,"value":37},{"type":21,"value":337},", and restart the browser now and then if your jobs run for days.",{"type":16,"tag":17,"props":339,"children":340},{},[341,343,349],{"type":21,"value":342},"We ran these tests on an Apple M5 Pro with 64 GB of RAM, on macOS, with Node 26 and Puppeteer 25.16.0. Memory is the sum over the browser's process tree. Our test environment blocked Chrome's sandbox, so these runs used ",{"type":16,"tag":24,"props":344,"children":346},{"className":345},[],[347],{"type":21,"value":348},"--no-sandbox",{"type":21,"value":90},{"type":16,"tag":69,"props":351,"children":353},{"id":352},"should-i-launch-a-new-browser-for-every-pdf",[354],{"type":21,"value":355},"Should I launch a new browser for every PDF?",{"type":16,"tag":17,"props":357,"children":358},{},[359],{"type":21,"value":360},"No. You pay for the launch on every job, and a page on a running browser does the same work.",{"type":16,"tag":17,"props":362,"children":363},{},[364,366,372],{"type":21,"value":365},"With ",{"type":16,"tag":24,"props":367,"children":369},{"className":368},[],[370],{"type":21,"value":371},"networkidle0",{"type":21,"value":373}," as the wait, launching a browser, converting and closing it took 1.47 seconds per job (the median of 30 runs). Reusing one browser cut that to 1.06 seconds (100 runs). So the launch alone cost about 0.4 seconds.",{"type":16,"tag":17,"props":375,"children":376},{},[377,379,384,386,391],{"type":21,"value":378},"The wait cost more than the launch. With ",{"type":16,"tag":24,"props":380,"children":382},{"className":381},[],[383],{"type":21,"value":45},{"type":21,"value":385}," followed by ",{"type":16,"tag":24,"props":387,"children":389},{"className":388},[],[390],{"type":21,"value":53},{"type":21,"value":392},", the reused browser took 0.24 seconds per job (100 runs), for the same 121.5 KB PDF.",{"type":16,"tag":142,"props":394,"children":396},{"caption":395},"Median time per PDF in seconds, same 30-row invoice. Our test, Puppeteer 25.16.0, November 4, 2026.",[397],{"type":16,"tag":147,"props":398,"children":404},{"viewBox":399,"xmlns":150,"role":151,"ariaLabelledBy":400,"style":403,"fontFamily":156},"0 0 560 210",[401,402],"pptr-time-title","pptr-time-desc","max-width:560px;min-width:460px;display:block;margin:0 auto",[405,410,415,436,442,446,451],{"type":16,"tag":159,"props":406,"children":407},{"id":401},[408],{"type":21,"value":409},"Reusing the browser and waiting for load cut each PDF from 1.47 s to 0.24 s",{"type":16,"tag":165,"props":411,"children":412},{"id":402},[413],{"type":21,"value":414},"Bar chart of median seconds per PDF. New browser per job, networkidle0: 1.47 s (30 runs). Reused browser, networkidle0: 1.06 s (100 runs). Reused browser, load then fonts ready: 0.24 s (100 runs).",{"type":16,"tag":171,"props":416,"children":417},{"fontSize":173,"fill":312},[418,424,430],{"type":16,"tag":21,"props":419,"children":421},{"x":207,"y":420},"30",[422],{"type":21,"value":423},"New browser per job, networkidle0: 1.47 s",{"type":16,"tag":21,"props":425,"children":427},{"x":207,"y":426},"92",[428],{"type":21,"value":429},"Reused browser, networkidle0: 1.06 s",{"type":16,"tag":21,"props":431,"children":433},{"x":207,"y":432},"154",[434],{"type":21,"value":435},"Reused browser, load + fonts ready: 0.24 s",{"type":16,"tag":302,"props":437,"children":441},{"x":207,"y":438,"width":439,"height":440,"rx":276,"fill":261},"40","540","22",[],{"type":16,"tag":302,"props":443,"children":445},{"x":207,"y":310,"width":444,"height":440,"rx":276,"fill":174},"389",[],{"type":16,"tag":302,"props":447,"children":450},{"x":207,"y":448,"width":449,"height":440,"rx":276,"fill":267},"164","88",[],{"type":16,"tag":177,"props":452,"children":454},{"x1":207,"y1":453,"x2":207,"y2":186,"stroke":182},"34",[],{"type":16,"tag":17,"props":456,"children":457},{},[458,460,465],{"type":21,"value":459},"Then run pages in parallel on that one browser. With 10 pages at a time and the same ",{"type":16,"tag":24,"props":461,"children":463},{"className":462},[],[464],{"type":21,"value":371},{"type":21,"value":466}," wait, 100 jobs took 13.9 seconds: about 7 PDFs a second, against about 1 a second one at a time. Each job got slower (median 1.16 seconds) and memory peaked around 678 MB. To find your limit, keep raising the number of parallel pages until memory or per-job time gets worse than you'll accept, then scale out with more processes.",{"type":16,"tag":69,"props":468,"children":470},{"id":469},"which-waituntil-should-i-use-with-pagepdf",[471],{"type":21,"value":472},"Which waitUntil should I use with page.pdf()?",{"type":16,"tag":17,"props":474,"children":475},{},[476,478,483,485,490,492,497],{"type":21,"value":477},"Use ",{"type":16,"tag":24,"props":479,"children":481},{"className":480},[],[482],{"type":21,"value":45},{"type":21,"value":484},", then wait for ",{"type":16,"tag":24,"props":486,"children":488},{"className":487},[],[489],{"type":21,"value":53},{"type":21,"value":491},". ",{"type":16,"tag":24,"props":493,"children":495},{"className":494},[],[496],{"type":21,"value":45},{"type":21,"value":498}," fires once the page's stylesheets, scripts and images have loaded, except lazy-loaded ones. The fonts wait covers web fonts the page asked for.",{"type":16,"tag":17,"props":500,"children":501},{},[502],{"type":21,"value":503},"The two other common choices each cost you something:",{"type":16,"tag":505,"props":506,"children":507},"ul",{},[508,539],{"type":16,"tag":509,"props":510,"children":511},"li",{},[512,522,524,529,531,537],{"type":16,"tag":513,"props":514,"children":515},"strong",{},[516],{"type":16,"tag":24,"props":517,"children":519},{"className":518},[],[520],{"type":21,"value":521},"domcontentloaded",{"type":21,"value":523}," doesn't wait for stylesheets to load. In 10 runs out of 10, our invoice came out without its web font, and the PDF was 82.6 KB instead of 121.5 KB. ",{"type":16,"tag":24,"props":525,"children":527},{"className":526},[],[528],{"type":21,"value":29},{"type":21,"value":530}," waits for fonts by default (its ",{"type":16,"tag":24,"props":532,"children":534},{"className":533},[],[535],{"type":21,"value":536},"waitForFonts",{"type":21,"value":538}," option), and that didn't change the result.",{"type":16,"tag":509,"props":540,"children":541},{},[542,550,552,559],{"type":16,"tag":513,"props":543,"children":544},{},[545],{"type":16,"tag":24,"props":546,"children":548},{"className":547},[],[549],{"type":21,"value":371},{"type":21,"value":551}," waits until the page has had ",{"type":16,"tag":81,"props":553,"children":556},{"href":554,"rel":555},"https://pptr.dev/api/puppeteer.puppeteerlifecycleevent",[85],[557],{"type":21,"value":558},"no network connections for 500 ms",{"type":21,"value":560},". The fonts arrive, but every job pays for that quiet period. A page that keeps a connection open, an analytics socket for example, may never go quiet, and the job runs until it times out.",{"type":16,"tag":17,"props":562,"children":563},{},[564,566,571,573,579,581,587,589,595],{"type":21,"value":565},"Puppeteer's TypeScript types don't allow ",{"type":16,"tag":24,"props":567,"children":569},{"className":568},[],[570],{"type":21,"value":371},{"type":21,"value":572}," or ",{"type":16,"tag":24,"props":574,"children":576},{"className":575},[],[577],{"type":21,"value":578},"networkidle2",{"type":21,"value":580}," with ",{"type":16,"tag":24,"props":582,"children":584},{"className":583},[],[585],{"type":21,"value":586},"setContent()",{"type":21,"value":588},", though plain JavaScript accepted it in our runs. For a URL, ",{"type":16,"tag":24,"props":590,"children":592},{"className":591},[],[593],{"type":21,"value":594},"page.goto(url, { waitUntil: 'load' })",{"type":21,"value":596}," followed by the same fonts wait works the same way.",{"type":16,"tag":17,"props":598,"children":599},{},[600,602,607,609,619,621,626],{"type":21,"value":601},"Whichever you pick, keep the timeouts. Navigation and ",{"type":16,"tag":24,"props":603,"children":605},{"className":604},[],[606],{"type":21,"value":586},{"type":21,"value":608}," default to 30 seconds, and so does ",{"type":16,"tag":81,"props":610,"children":613},{"href":611,"rel":612},"https://pptr.dev/api/puppeteer.pdfoptions",[85],[614],{"type":16,"tag":24,"props":615,"children":617},{"className":616},[],[618],{"type":21,"value":29},{"type":21,"value":620},". Passing ",{"type":16,"tag":24,"props":622,"children":624},{"className":623},[],[625],{"type":21,"value":207},{"type":21,"value":627}," turns a timeout off, which is how a single bad page holds a worker forever.",{"type":16,"tag":69,"props":629,"children":631},{"id":630},"why-are-backgrounds-page-sizes-or-headers-wrong-in-the-pdf",[632],{"type":21,"value":633},"Why are backgrounds, page sizes or headers wrong in the PDF?",{"type":16,"tag":17,"props":635,"children":636},{},[637],{"type":21,"value":638},"Most of the time it's a default.",{"type":16,"tag":505,"props":640,"children":641},{},[642,675,707,757],{"type":16,"tag":509,"props":643,"children":644},{},[645,650,652,658,660,665,667,673],{"type":16,"tag":513,"props":646,"children":647},{},[648],{"type":21,"value":649},"Missing colors or background images:",{"type":21,"value":651}," ",{"type":16,"tag":24,"props":653,"children":655},{"className":654},[],[656],{"type":21,"value":657},"printBackground",{"type":21,"value":659}," is off by default, so colored table headers and background images disappear. Turn it on. If colors still look washed out, that's ",{"type":16,"tag":24,"props":661,"children":663},{"className":662},[],[664],{"type":21,"value":29},{"type":21,"value":666}," adjusting them for print: add ",{"type":16,"tag":24,"props":668,"children":670},{"className":669},[],[671],{"type":21,"value":672},"-webkit-print-color-adjust: exact",{"type":21,"value":674}," to your CSS.",{"type":16,"tag":509,"props":676,"children":677},{},[678,683,684,689,691,697,699,705],{"type":16,"tag":513,"props":679,"children":680},{},[681],{"type":21,"value":682},"Screen styles that don't show:",{"type":21,"value":651},{"type":16,"tag":24,"props":685,"children":687},{"className":686},[],[688],{"type":21,"value":29},{"type":21,"value":690}," renders with the print media type, so your ",{"type":16,"tag":24,"props":692,"children":694},{"className":693},[],[695],{"type":21,"value":696},"@media print",{"type":21,"value":698}," rules apply and ",{"type":16,"tag":24,"props":700,"children":702},{"className":701},[],[703],{"type":21,"value":704},"@media screen",{"type":21,"value":706}," ones don't.",{"type":16,"tag":509,"props":708,"children":709},{},[710,715,717,723,725,731,733,739,741,747,749,755],{"type":16,"tag":513,"props":711,"children":712},{},[713],{"type":21,"value":714},"The wrong page size:",{"type":21,"value":716}," Letter is the default, and ",{"type":16,"tag":24,"props":718,"children":720},{"className":719},[],[721],{"type":21,"value":722},"format",{"type":21,"value":724}," wins over ",{"type":16,"tag":24,"props":726,"children":728},{"className":727},[],[729],{"type":21,"value":730},"width",{"type":21,"value":732}," and ",{"type":16,"tag":24,"props":734,"children":736},{"className":735},[],[737],{"type":21,"value":738},"height",{"type":21,"value":740},". If your CSS has an ",{"type":16,"tag":24,"props":742,"children":744},{"className":743},[],[745],{"type":21,"value":746},"@page { size: A4 }",{"type":21,"value":748}," rule you want to keep, set ",{"type":16,"tag":24,"props":750,"children":752},{"className":751},[],[753],{"type":21,"value":754},"preferCSSPageSize: true",{"type":21,"value":756},". Otherwise Puppeteer scales your content to fit the paper size it was given.",{"type":16,"tag":509,"props":758,"children":759},{},[760,765,767,773,775,781,782,788,789,795,796,802,804,810,812,817,818,824],{"type":16,"tag":513,"props":761,"children":762},{},[763],{"type":21,"value":764},"Headers and footers:",{"type":21,"value":766}," turn on ",{"type":16,"tag":24,"props":768,"children":770},{"className":769},[],[771],{"type":21,"value":772},"displayHeaderFooter",{"type":21,"value":774}," and put elements with the classes ",{"type":16,"tag":24,"props":776,"children":778},{"className":777},[],[779],{"type":21,"value":780},"pageNumber",{"type":21,"value":732},{"type":16,"tag":24,"props":783,"children":785},{"className":784},[],[786],{"type":21,"value":787},"totalPages",{"type":21,"value":133},{"type":16,"tag":24,"props":790,"children":792},{"className":791},[],[793],{"type":21,"value":794},"headerTemplate",{"type":21,"value":572},{"type":16,"tag":24,"props":797,"children":799},{"className":798},[],[800],{"type":21,"value":801},"footerTemplate",{"type":21,"value":803},". The classes ",{"type":16,"tag":24,"props":805,"children":807},{"className":806},[],[808],{"type":21,"value":809},"date",{"type":21,"value":811},", ",{"type":16,"tag":24,"props":813,"children":815},{"className":814},[],[816],{"type":21,"value":159},{"type":21,"value":732},{"type":16,"tag":24,"props":819,"children":821},{"className":820},[],[822],{"type":21,"value":823},"url",{"type":21,"value":825}," work the same way.",{"type":16,"tag":69,"props":827,"children":829},{"id":828},"what-breaks-when-puppeteer-runs-in-docker-or-serverless",[830],{"type":21,"value":831},"What breaks when Puppeteer runs in Docker or serverless?",{"type":16,"tag":17,"props":833,"children":834},{},[835],{"type":21,"value":836},"Your laptop has everything Chrome needs. A slim container image doesn't. Here's the checklist:",{"type":16,"tag":505,"props":838,"children":839},{},[840,858,892,910,956,966,998],{"type":16,"tag":509,"props":841,"children":842},{},[843,848,850,856],{"type":16,"tag":513,"props":844,"children":845},{},[846],{"type":21,"value":847},"Missing libraries.",{"type":21,"value":849}," Chrome won't start without them. Run ",{"type":16,"tag":24,"props":851,"children":853},{"className":852},[],[854],{"type":21,"value":855},"ldd chrome | grep not",{"type":21,"value":857}," on the browser binary to list the ones you're missing.",{"type":16,"tag":509,"props":859,"children":860},{},[861,866,868,874,876,883,885,890],{"type":16,"tag":513,"props":862,"children":863},{},[864],{"type":21,"value":865},"No usable sandbox.",{"type":21,"value":867}," Chrome crashes with ",{"type":16,"tag":24,"props":869,"children":871},{"className":870},[],[872],{"type":21,"value":873},"No usable sandbox!",{"type":21,"value":875}," when the container can't give it one. Puppeteer's docs say running without one ",{"type":16,"tag":81,"props":877,"children":880},{"href":878,"rel":879},"https://pptr.dev/troubleshooting",[85],[881],{"type":21,"value":882},"is \"strongly discouraged\"",{"type":21,"value":884}," and accept ",{"type":16,"tag":24,"props":886,"children":888},{"className":887},[],[889],{"type":21,"value":348},{"type":21,"value":891}," only when you trust all the content you render. If users can send you HTML, set up the sandbox instead. On Ubuntu 23.10 and later, AppArmor blocks the sandbox for the Chrome that Puppeteer downloads, which gives the same error.",{"type":16,"tag":509,"props":893,"children":894},{},[895,900,902,908],{"type":16,"tag":513,"props":896,"children":897},{},[898],{"type":21,"value":899},"Zombie processes.",{"type":21,"value":901}," Start the container with ",{"type":16,"tag":24,"props":903,"children":905},{"className":904},[],[906],{"type":21,"value":907},"--init",{"type":21,"value":909},", or a custom entrypoint, so finished Chrome processes get cleaned up instead of piling up. Puppeteer's Docker guide runs it that way.",{"type":16,"tag":509,"props":911,"children":912},{},[913,918,920,926,927,933,934,940,941,947,948,954],{"type":16,"tag":513,"props":914,"children":915},{},[916],{"type":21,"value":917},"Fonts.",{"type":21,"value":919}," Chinese, Japanese, Arabic, Hebrew or Thai text needs fonts a slim image doesn't have. Puppeteer's troubleshooting page installs ",{"type":16,"tag":24,"props":921,"children":923},{"className":922},[],[924],{"type":21,"value":925},"fonts-ipafont-gothic",{"type":21,"value":811},{"type":16,"tag":24,"props":928,"children":930},{"className":929},[],[931],{"type":21,"value":932},"fonts-wqy-zenhei",{"type":21,"value":811},{"type":16,"tag":24,"props":935,"children":937},{"className":936},[],[938],{"type":21,"value":939},"fonts-thai-tlwg",{"type":21,"value":811},{"type":16,"tag":24,"props":942,"children":944},{"className":943},[],[945],{"type":21,"value":946},"fonts-kacst",{"type":21,"value":732},{"type":16,"tag":24,"props":949,"children":951},{"className":950},[],[952],{"type":21,"value":953},"fonts-freefont-ttf",{"type":21,"value":955}," for those, plus the fonts your own templates use.",{"type":16,"tag":509,"props":957,"children":958},{},[959,964],{"type":16,"tag":513,"props":960,"children":961},{},[962],{"type":21,"value":963},"Alpine.",{"type":21,"value":965}," Chrome doesn't support Alpine out of the box. Use a Debian-based image.",{"type":16,"tag":509,"props":967,"children":968},{},[969,981,983,989,991,996],{"type":16,"tag":513,"props":970,"children":971},{},[972,974,980],{"type":21,"value":973},"The browser isn't in ",{"type":16,"tag":24,"props":975,"children":977},{"className":976},[],[978],{"type":21,"value":979},"node_modules",{"type":21,"value":90},{"type":21,"value":982}," Puppeteer downloads Chrome into ",{"type":16,"tag":24,"props":984,"children":986},{"className":985},[],[987],{"type":21,"value":988},"~/.cache/puppeteer",{"type":21,"value":990},", so a multi-stage build that copies only ",{"type":16,"tag":24,"props":992,"children":994},{"className":993},[],[995],{"type":21,"value":979},{"type":21,"value":997}," ships without a browser.",{"type":16,"tag":509,"props":999,"children":1000},{},[1001,1006,1008,1015],{"type":16,"tag":513,"props":1002,"children":1003},{},[1004],{"type":21,"value":1005},"Serverless size limits.",{"type":21,"value":1007}," An AWS Lambda deployment package is ",{"type":16,"tag":81,"props":1009,"children":1012},{"href":1010,"rel":1011},"https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html",[85],[1013],{"type":21,"value":1014},"capped at 250 MB unzipped",{"type":21,"value":1016},", layers included, so check the size of the Chrome build you ship against it before you pick Lambda.",{"type":16,"tag":17,"props":1018,"children":1019},{},[1020,1022,1029],{"type":21,"value":1021},"Then pin your versions. Chrome moved to ",{"type":16,"tag":81,"props":1023,"children":1026},{"href":1024,"rel":1025},"https://developer.chrome.com/blog/chrome-two-week-release",[85],[1027],{"type":21,"value":1028},"a release every two weeks",{"type":21,"value":1030}," with Chrome 153 in September 2026, and each Puppeteer release targets one Chrome version. Upgrade on purpose, and compare a few real PDFs before and after.",{"type":16,"tag":69,"props":1032,"children":1034},{"id":1033},"when-is-running-chromium-yourself-not-worth-it",[1035],{"type":21,"value":1036},"When is running Chromium yourself not worth it?",{"type":16,"tag":17,"props":1038,"children":1039},{},[1040],{"type":21,"value":1041},"When the PDF is a feature of your product and not the product itself. Every section above is something you now maintain, for a feature your users see as one button.",{"type":16,"tag":17,"props":1043,"children":1044},{},[1045],{"type":21,"value":1046},"People don't buy PDFs. They buy not having to run Chromium. That's the job we took on at PDFShift, and we've run Chromium conversions since 2018. You send one request with your HTML or a URL to a Chromium we run, and get the PDF back:",{"type":16,"tag":92,"props":1048,"children":1051},{"className":1049,"code":1050,"language":97,"meta":7},[95],"const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {\n    method: 'POST',\n    headers: {\n        'X-API-Key': process.env.PDFSHIFT_API_KEY,\n        'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ source: html }), // raw HTML or a URL\n});\n\nif (!response.ok) {\n    throw new Error(`PDFShift returned ${response.status}: ${await response.text()}`);\n}\n\nconst pdf = Buffer.from(await response.arrayBuffer());\n",[1052],{"type":16,"tag":24,"props":1053,"children":1054},{"__ignoreMap":7},[1055],{"type":21,"value":1050},{"type":16,"tag":17,"props":1057,"children":1058},{},[1059,1061,1068,1070,1076,1078,1083,1085,1091,1093,1099],{"type":21,"value":1060},"The request format is in the ",{"type":16,"tag":81,"props":1062,"children":1065},{"href":1063,"rel":1064},"https://docs.pdfshift.io/api-reference/convert-to-pdf",[85],[1066],{"type":21,"value":1067},"conversion API reference",{"type":21,"value":1069},", and there are more examples for ",{"type":16,"tag":81,"props":1071,"children":1073},{"href":1072},"/guides/node/node-fetch/",[1074],{"type":21,"value":1075},"Node with fetch",{"type":21,"value":1077}," and the other libraries in our ",{"type":16,"tag":81,"props":1079,"children":1081},{"href":1080},"/guides/",[1082],{"type":21,"value":5},{"type":21,"value":1084},". The free plan has 50 credits a month with no credit card, and the ",{"type":16,"tag":81,"props":1086,"children":1088},{"href":1087},"/pricing/",[1089],{"type":21,"value":1090},"paid plans",{"type":21,"value":1092}," start at $9 a month. ",{"type":16,"tag":81,"props":1094,"children":1096},{"href":1095},"/register/",[1097],{"type":21,"value":1098},"Get a free API key",{"type":21,"value":1100}," and send your own invoice through it.",{"title":7,"searchDepth":1102,"depth":1102,"links":1103},2,[1104,1105,1106,1107,1108,1109,1110],{"id":71,"depth":1102,"text":74},{"id":105,"depth":1102,"text":108},{"id":352,"depth":1102,"text":355},{"id":469,"depth":1102,"text":472},{"id":630,"depth":1102,"text":633},{"id":828,"depth":1102,"text":831},{"id":1033,"depth":1102,"text":1036},"markdown","content:guides:puppeteer-pdf-in-production.md","content","guides/puppeteer-pdf-in-production.md","md",1793867698956]