[{"data":1,"prerenderedAt":533},["ShallowReactive",2],{"navigation":3,"/docs/configuration/common-tuning-strategies":96,"/docs/configuration/common-tuning-strategies-surround":528},[4],{"title":5,"path":6,"stem":7,"children":8,"page":43},"Docs","/docs","docs",[9,25,44,64,80,88],{"title":10,"path":11,"stem":12,"children":13,"icon":24},"Getting Started","/docs/getting-started","docs/1.getting-started/1.index",[14,16,20],{"title":15,"path":11,"stem":12},"Introduction",{"title":17,"path":18,"stem":19},"Quick start","/docs/getting-started/quick-start","docs/1.getting-started/2.quick-start",{"title":21,"path":22,"stem":23},"Running from source","/docs/getting-started/running-from-source","docs/1.getting-started/3.running-from-source","i-lucide-rocket",{"title":26,"icon":27,"path":28,"stem":29,"children":30,"page":43},"Configuration","i-lucide-settings-2","/docs/configuration","docs/2.configuration",[31,35,39],{"title":32,"path":33,"stem":34},"Adding a real database","/docs/configuration/adding-databases","docs/2.configuration/1.adding-databases",{"title":36,"path":37,"stem":38},"Customizing the image","/docs/configuration/customizing-the-image","docs/2.configuration/2.customizing-the-image",{"title":40,"path":41,"stem":42},"Common tuning strategies","/docs/configuration/common-tuning-strategies","docs/2.configuration/3.common-tuning-strategies",false,{"title":45,"path":46,"stem":47,"children":48,"icon":63},"Image variations","/docs/image-variations","docs/3.image-variations/1.index",[49,51,55,59],{"title":50,"path":46,"stem":47},"Choosing a variation",{"title":52,"path":53,"stem":54},"FPM-NGINX","/docs/image-variations/fpm-nginx","docs/3.image-variations/2.fpm-nginx",{"title":56,"path":57,"stem":58},"FrankenPHP","/docs/image-variations/frankenphp","docs/3.image-variations/3.frankenphp",{"title":60,"path":61,"stem":62},"FPM-Apache","/docs/image-variations/fpm-apache","docs/3.image-variations/4.fpm-apache","i-lucide-layers",{"title":65,"path":66,"stem":67,"children":68,"icon":79},"Benchmarks & Methodology","/docs/benchmarks","docs/4.benchmarks/1.index",[69,71,75],{"title":70,"path":66,"stem":67},"Methodology",{"title":72,"path":73,"stem":74},"Reading your results","/docs/benchmarks/reading-your-results","docs/4.benchmarks/2.reading-your-results",{"title":76,"path":77,"stem":78},"Testing from another machine","/docs/benchmarks/testing-from-another-machine","docs/4.benchmarks/3.testing-from-another-machine","i-lucide-flask-conical",{"title":81,"path":82,"stem":83,"children":84,"icon":87},"Community Results","/docs/community-results","docs/5.community-results/1.index",[85],{"title":86,"path":82,"stem":83},"Submitting your results","i-lucide-users",{"title":89,"path":90,"stem":91,"children":92,"icon":95},"FAQ","/docs/faq","docs/6.faq/1.index",[93],{"title":94,"path":90,"stem":91},"Frequently asked questions","i-lucide-circle-help",{"id":97,"title":40,"body":98,"description":520,"extension":521,"links":522,"meta":523,"navigation":524,"path":41,"redirect":522,"seo":525,"sitemap":526,"stem":42,"__hash__":527},"docs/docs/2.configuration/3.common-tuning-strategies.md",{"type":99,"value":100,"toc":512},"minimark",[101,113,116,122,127,139,150,249,256,260,263,266,269,289,292,357,366,372,376,379,459,462,466,473,479,485,489,492,498,502,505,508],[102,103,104,105,112],"p",{},"BenchKit is not a tuned server. It turns OPcache on, caches configuration and routes, and otherwise leaves the ",[106,107,111],"a",{"href":108,"rel":109},"https://serversideup.net/open-source/docker-php",[110],"nofollow","serversideup/php"," defaults where they are.",[102,114,115],{},"That is on purpose. A benchmark tuned by whoever wrote it measures their tuning, and every result it produces is really a result about their opinions. Leaving it plain means the number you get first is a baseline you can beat — and beating it is where the learning is. Change one thing, run again, put the two side by side.",[117,118,119],"note",{},[102,120,121],{},"Everything on this page is an environment variable. Change it, restart the container, and run again. Nothing here needs a rebuild.",[123,124,126],"h2",{"id":125},"the-worker-pool","The worker pool",[102,128,129,130,134,135,138],{},"On the ",[131,132,133],"code",{},"fpm-nginx"," and ",[131,136,137],{},"fpm-apache"," variations, PHP-FPM handles one request per worker for that request's whole duration — including any time it spends waiting on a database or an API. The pool size is therefore a hard ceiling on how many requests your server can have in flight, and no amount of CPU raises it.",[102,140,141,142,145,146,149],{},"You can see the ceiling on your own result. The ",[131,143,144],{},"/bench/io"," curve climbs in a straight line and then goes flat, and the flat part sits on a line drawn from arithmetic: ",[131,147,148],{},"workers ÷ delay",". Where the curve bends is your worker count.",[151,152,158],"pre",{"className":153,"code":154,"filename":155,"language":156,"meta":157,"style":157},"language-yaml shiki shiki-themes github-dark","environment:\n  # How the pool is managed: ondemand, dynamic, or static\n  PHP_FPM_PM_CONTROL: \"ondemand\"\n  # The ceiling. A worker is a process, so this is bounded by memory\n  PHP_FPM_PM_MAX_CHILDREN: \"20\"\n  # Only used by dynamic and ondemand\n  PHP_FPM_PM_START_SERVERS: \"2\"\n  PHP_FPM_PM_MIN_SPARE_SERVERS: \"1\"\n  PHP_FPM_PM_MAX_SPARE_SERVERS: \"3\"\n","docker-compose.yml","yaml","",[131,159,160,173,180,193,199,210,216,227,238],{"__ignoreMap":157},[161,162,165,169],"span",{"class":163,"line":164},"line",1,[161,166,168],{"class":167},"s4JwU","environment",[161,170,172],{"class":171},"s95oV",":\n",[161,174,176],{"class":163,"line":175},2,[161,177,179],{"class":178},"sAwPA","  # How the pool is managed: ondemand, dynamic, or static\n",[161,181,183,186,189],{"class":163,"line":182},3,[161,184,185],{"class":167},"  PHP_FPM_PM_CONTROL",[161,187,188],{"class":171},": ",[161,190,192],{"class":191},"sU2Wk","\"ondemand\"\n",[161,194,196],{"class":163,"line":195},4,[161,197,198],{"class":178},"  # The ceiling. A worker is a process, so this is bounded by memory\n",[161,200,202,205,207],{"class":163,"line":201},5,[161,203,204],{"class":167},"  PHP_FPM_PM_MAX_CHILDREN",[161,206,188],{"class":171},[161,208,209],{"class":191},"\"20\"\n",[161,211,213],{"class":163,"line":212},6,[161,214,215],{"class":178},"  # Only used by dynamic and ondemand\n",[161,217,219,222,224],{"class":163,"line":218},7,[161,220,221],{"class":167},"  PHP_FPM_PM_START_SERVERS",[161,223,188],{"class":171},[161,225,226],{"class":191},"\"2\"\n",[161,228,230,233,235],{"class":163,"line":229},8,[161,231,232],{"class":167},"  PHP_FPM_PM_MIN_SPARE_SERVERS",[161,234,188],{"class":171},[161,236,237],{"class":191},"\"1\"\n",[161,239,241,244,246],{"class":163,"line":240},9,[161,242,243],{"class":167},"  PHP_FPM_PM_MAX_SPARE_SERVERS",[161,245,188],{"class":171},[161,247,248],{"class":191},"\"3\"\n",[102,250,251,252,255],{},"Size ",[131,253,254],{},"PHP_FPM_PM_MAX_CHILDREN"," from memory rather than from cores. A worker is a process holding a copy of your application — measured on this image, roughly 40 MB each — so twenty workers want about 800 MB before they serve anything. Set it past what your machine has and you trade a slow server for one the kernel kills.",[123,257,259],{"id":258},"a-worked-example-that-goes-the-wrong-way","A worked example that goes the wrong way",[102,261,262],{},"Here is a real change on a real machine, because it is more useful than a list of settings that \"improve performance\".",[102,264,265],{},"The observation: on a four-core box, response times were fine when idle and poor once busy. A single request came back in 13 ms; at 70% of what the machine could serve, the slowest 5% took 161 ms. That is the shape of a queue, and one likely cause is a pool that starts workers on demand — every time traffic outruns the few spare workers, PHP-FPM forks a new one and boots the application before it can answer anything.",[102,267,268],{},"So the obvious fix is a pool that stays warm:",[151,270,272],{"className":153,"code":271,"filename":155,"language":156,"meta":157,"style":157},"environment:\n  PHP_FPM_PM_CONTROL: \"static\"\n",[131,273,274,280],{"__ignoreMap":157},[161,275,276,278],{"class":163,"line":164},[161,277,168],{"class":167},[161,279,172],{"class":171},[161,281,282,284,286],{"class":163,"line":175},[161,283,185],{"class":167},[161,285,188],{"class":171},[161,287,288],{"class":191},"\"static\"\n",[102,290,291],{},"It made things worse.",[293,294,295,315],"table",{},[296,297,298],"thead",{},[299,300,301,304,310],"tr",{},[302,303],"th",{},[302,305,307],{"align":306},"right",[131,308,309],{},"ondemand",[302,311,312],{"align":306},[131,313,314],{},"static",[316,317,318,333,346],"tbody",{},[299,319,320,324,327],{},[321,322,323],"td",{},"Response time",[321,325,326],{"align":306},"26 ms",[321,328,329],{"align":306},[330,331,332],"strong",{},"83 ms",[299,334,335,338,341],{},[321,336,337],{},"Slowest 5%",[321,339,340],{"align":306},"141 ms",[321,342,343],{"align":306},[330,344,345],{},"173 ms",[299,347,348,351,354],{},[321,349,350],{},"Max throughput",[321,352,353],{"align":306},"437 req/s",[321,355,356],{"align":306},"462 req/s",[102,358,359,360,362,363,365],{},"Throughput barely moved and the median response time tripled. The reason was in the same result's environment panel: the machine had 3.9 GB of memory and was already using swap. ",[131,361,314],{}," forks all twenty workers at boot and keeps them resident, and 800 MB of resident PHP was memory that box did not have. ",[131,364,309],{}," was the right setting for it all along.",[367,368,369],"tip",{},[102,370,371],{},"This is the point of running the benchmark rather than copying settings. The advice \"keep the pool warm\" is sound and it was wrong here, because whether it is right depends on memory you have and the person giving the advice does not.",[123,373,375],{"id":374},"opcache","OPcache",[102,377,378],{},"BenchKit turns OPcache on by default, because practically no production host runs without it and a benchmark measuring PHP recompiling your application on every request is measuring nothing anyone deploys.",[151,380,382],{"className":153,"code":381,"filename":155,"language":156,"meta":157,"style":157},"environment:\n  PHP_OPCACHE_ENABLE: \"1\"\n  # Off in production: PHP then never stats your files for changes\n  PHP_OPCACHE_VALIDATE_TIMESTAMPS: \"0\"\n  PHP_OPCACHE_MEMORY_CONSUMPTION: \"256\"\n  PHP_OPCACHE_MAX_ACCELERATED_FILES: \"20000\"\n  # Rarely helps a typical web request — worth measuring rather than assuming\n  PHP_OPCACHE_JIT: \"tracing\"\n  PHP_OPCACHE_JIT_BUFFER_SIZE: \"64M\"\n",[131,383,384,390,399,404,414,424,434,439,449],{"__ignoreMap":157},[161,385,386,388],{"class":163,"line":164},[161,387,168],{"class":167},[161,389,172],{"class":171},[161,391,392,395,397],{"class":163,"line":175},[161,393,394],{"class":167},"  PHP_OPCACHE_ENABLE",[161,396,188],{"class":171},[161,398,237],{"class":191},[161,400,401],{"class":163,"line":182},[161,402,403],{"class":178},"  # Off in production: PHP then never stats your files for changes\n",[161,405,406,409,411],{"class":163,"line":195},[161,407,408],{"class":167},"  PHP_OPCACHE_VALIDATE_TIMESTAMPS",[161,410,188],{"class":171},[161,412,413],{"class":191},"\"0\"\n",[161,415,416,419,421],{"class":163,"line":201},[161,417,418],{"class":167},"  PHP_OPCACHE_MEMORY_CONSUMPTION",[161,420,188],{"class":171},[161,422,423],{"class":191},"\"256\"\n",[161,425,426,429,431],{"class":163,"line":212},[161,427,428],{"class":167},"  PHP_OPCACHE_MAX_ACCELERATED_FILES",[161,430,188],{"class":171},[161,432,433],{"class":191},"\"20000\"\n",[161,435,436],{"class":163,"line":218},[161,437,438],{"class":178},"  # Rarely helps a typical web request — worth measuring rather than assuming\n",[161,440,441,444,446],{"class":163,"line":229},[161,442,443],{"class":167},"  PHP_OPCACHE_JIT",[161,445,188],{"class":171},[161,447,448],{"class":191},"\"tracing\"\n",[161,450,451,454,456],{"class":163,"line":240},[161,452,453],{"class":167},"  PHP_OPCACHE_JIT_BUFFER_SIZE",[161,455,188],{"class":171},[161,457,458],{"class":191},"\"64M\"\n",[102,460,461],{},"JIT is the one most often expected to help and most often does not. A typical Laravel request spends its time on I/O, autoloading and array work rather than in the kind of tight numeric loop JIT is good at. Turn it on, run again, and see — that is a two-minute experiment and a better answer than anybody's opinion.",[123,463,465],{"id":464},"worker-mode-frankenphp-and-octane","Worker mode: FrankenPHP and Octane",[102,467,468,469,472],{},"The ",[131,470,471],{},"frankenphp"," variation can run in worker mode, where the framework boots once and stays in memory across requests instead of booting per request. That removes the fixed cost of bootstrapping Laravel from every single request, which is most of what a small JSON response costs.",[102,474,475,476,478],{},"It is a different execution model rather than a faster version of the same one, and it comes with the constraints of long-lived processes — no request state in singletons or statics. See ",[106,477,56],{"href":57}," for how to run it and what to watch.",[480,481,482],"warning",{},[102,483,484],{},"When you compare a variation against another, tune both sides first. A default FPM pool against a default FrankenPHP tells you about two sets of defaults, not about two servers.",[123,486,488],{"id":487},"the-database","The database",[102,490,491],{},"BenchKit uses SQLite unless you point it elsewhere. SQLite runs inside the PHP process, so it has no connection cost and none of the contention behaviour of a database server — which makes it a poor stand-in if your application talks to MySQL or Postgres.",[102,493,494,495,497],{},"See ",[106,496,32],{"href":33}," for compose recipes. The gallery lets you filter by engine, because a result measured against SQLite and one measured against Postgres are not on the same axis.",[123,499,501],{"id":500},"how-to-tell-whether-it-helped","How to tell whether it helped",[102,503,504],{},"Run, change one thing, run again, and open the two runs side by side in the compare view.",[102,506,507],{},"Watch the response time and the throughput separately. They move independently and they can move in opposite directions — the example above bought 25 req/s and paid 57 ms for it. Which of those you want depends on what you are building, and the benchmark's job is to show you the trade rather than to pick for you.",[509,510,511],"style",{},"html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":157,"searchDepth":175,"depth":175,"links":513},[514,515,516,517,518,519],{"id":125,"depth":175,"text":126},{"id":258,"depth":175,"text":259},{"id":374,"depth":175,"text":375},{"id":464,"depth":175,"text":465},{"id":487,"depth":175,"text":488},{"id":500,"depth":175,"text":501},"BenchKit ships deliberately untuned. Here is what to change, and how to tell whether it helped.","md",null,{},true,{"title":40,"description":520},{"loc":41},"r5GhhnfXU-qB2hMtNwYiAFriCs9iqYwoZwxBltD_l0U",[529,531],{"title":36,"path":37,"stem":38,"description":530,"children":-1},"Tune PHP, OPcache, and the web server with environment variables, or add your own php.ini.",{"title":50,"path":46,"stem":47,"description":532,"children":-1},"BenchKit ships the same application on three web servers and every PHP version it supports. The image tag picks both.",1787761544505]