├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── benchmarks.yml │ ├── ci.yml │ └── metrics.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── METRICS.md ├── README.md ├── benchmark-bench.js ├── benchmark-compare.js ├── benchmark-results.json ├── benchmark.js ├── benchmarks ├── 0http.cjs ├── @leizm-web.cjs ├── adonisjs.mjs ├── connect-router.cjs ├── connect.cjs ├── express-with-middlewares.cjs ├── express.cjs ├── fastify-big-json.cjs ├── fastify.cjs ├── h3-router.cjs ├── h3.cjs ├── hapi.cjs ├── hono.mjs ├── koa-isomorphic-router.cjs ├── koa-router.cjs ├── koa.cjs ├── micro-route.cjs ├── micro.cjs ├── microrouter.cjs ├── node-http.cjs ├── polka.cjs ├── polkadot.cjs ├── rayo.cjs ├── restana.cjs ├── restify.cjs ├── server-base-router.cjs ├── server-base.cjs ├── srvx.mjs ├── take-five.cjs ├── tinyhttp.mjs ├── trpc-router.cjs └── whatwg-node-server.mjs ├── lib ├── autocannon.js ├── bench.js └── packages.js ├── metrics ├── .gitignore ├── process-results.cjs ├── startup-listen.cjs ├── startup-routes-schema.cjs ├── startup-routes.cjs └── startup.cjs └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Node benchmarks 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | # * is a special character in YAML so you have to quote this string 10 | - cron: '0 0 1 * *' 11 | 12 | # This allows a subsequently queued workflow run to interrupt previous runs 13 | concurrency: 14 | group: "${{ github.workflow }}" 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | name: Build 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: write 26 | steps: 27 | - name: Check out repo 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | 30 | - name: Setup Node 31 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 32 | with: 33 | check-latest: true 34 | node-version: 20 35 | 36 | - name: Install dependencies 37 | run: npm i --ignore-scripts 38 | 39 | - name: Run benchmarks 40 | run: npm start y 100 10 40 41 | 42 | - name: Compare results 43 | run: | 44 | node ./benchmark compare -t 45 | node ./benchmark compare -u 46 | 47 | - name: Commit and push updated results 48 | uses: github-actions-x/commit@722d56b8968bf00ced78407bbe2ead81062d8baa # v2.9 49 | with: 50 | github-token: ${{ secrets.GITHUB_TOKEN }} 51 | push-branch: 'main' 52 | commit-message: 'chore: update benchmark results' 53 | force-add: 'true' 54 | rebase: 'true' 55 | files: benchmark-results.json README.md 56 | name: Github Actions 57 | email: <> 58 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | # This allows a subsequently queued workflow run to interrupt previous runs 6 | concurrency: 7 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | name: Dependency Review 16 | if: github.event_name == 'pull_request' 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | steps: 21 | - name: Check out repo 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Dependency review 27 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 28 | 29 | test: 30 | name: Test 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: read 34 | strategy: 35 | matrix: 36 | node-version: [20, 22, 24] 37 | steps: 38 | - name: Check out repo 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | persist-credentials: false 42 | 43 | - name: Setup Node ${{ matrix.node-version }} 44 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 45 | with: 46 | check-latest: true 47 | node-version: ${{ matrix.node-version }} 48 | 49 | - name: Install dependencies 50 | run: npm i --ignore-scripts 51 | 52 | - name: Run tests 53 | run: npm test && npm start y 1 1 1 54 | 55 | automerge: 56 | name: Automerge Dependabot PRs 57 | if: > 58 | github.event_name == 'pull_request' && 59 | github.event.pull_request.user.login == 'dependabot[bot]' 60 | needs: test 61 | permissions: 62 | pull-requests: write 63 | contents: write 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 67 | with: 68 | github-token: ${{ secrets.GITHUB_TOKEN }} 69 | target: minor 70 | -------------------------------------------------------------------------------- /.github/workflows/metrics.yml: -------------------------------------------------------------------------------- 1 | name: Node Metrics 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | # This allows a subsequently queued workflow run to interrupt previous runs 10 | concurrency: 11 | group: "${{ github.workflow }}" 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | name: Build 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: write 23 | steps: 24 | - name: Check out repo 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | 27 | - name: Setup Node 28 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 29 | with: 30 | check-latest: true 31 | node-version: 20 32 | 33 | - name: Install dependencies 34 | run: npm i --ignore-scripts 35 | 36 | - name: Run metrics 37 | run: | 38 | npm run metrics:run 39 | npm run metrics:summary 40 | 41 | - name: Commit and push updated results 42 | uses: github-actions-x/commit@722d56b8968bf00ced78407bbe2ead81062d8baa # v2.9 43 | with: 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | push-branch: 'main' 46 | commit-message: 'chore: update metrics results' 47 | force-add: 'true' 48 | rebase: 'true' 49 | files: METRICS.md 50 | name: Github Actions 51 | email: <> 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | 154 | # 0x 155 | profile-* 156 | 157 | # flamegraphs 158 | profile* 159 | 160 | # benchmark results 161 | results 162 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | results 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=false 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cagataycali@icloud.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ./c² 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /METRICS.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | * __Machine:__ linux x64 | 4 vCPUs | 15.6GB Mem 3 | * __Node:__ `v20.19.1` 4 | * __Run:__ Thu May 15 2025 14:01:37 GMT+0000 (Coordinated Universal Time) 5 | * __Method:__ `npm run metrics` (samples: 5) 6 | * __startup:__ time elapsed to setup the application 7 | * __listen:__ time elapsed until the http server is ready to accept requests (cold start) 8 | 9 | | | startup(ms) | listen(ms) | 10 | |-| - | - | 11 | | 1-startup-routes-schema.cjs | 95.47 | 131.16 | 12 | | 1-startup-routes.cjs | 95.46 | 105.69 | 13 | | 10-startup-routes-schema.cjs | 103.17 | 139.72 | 14 | | 10-startup-routes.cjs | 97.17 | 109.25 | 15 | | 100-startup-routes-schema.cjs | 109.47 | 150.71 | 16 | | 100-startup-routes.cjs | 108.86 | 128.63 | 17 | | 1000-startup-routes-schema.cjs | 278.73 | 378.15 | 18 | | 1000-startup-routes.cjs | 265.69 | 375.88 | 19 | | 10000-startup-routes-schema.cjs | 4564.27 | 4839.66 | 20 | | 10000-startup-routes.cjs | 4416.71 | 5786.88 | 21 | | startup-listen.cjs | 98.44 | 109.43 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | 10 |
11 | 12 | [![CI](https://github.com/fastify/fastify/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify/actions/workflows/ci.yml) 13 | [![Package Manager 14 | CI](https://github.com/fastify/fastify/actions/workflows/package-manager-ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify/actions/workflows/package-manager-ci.yml) 15 | [![Web 16 | site](https://github.com/fastify/fastify/actions/workflows/website.yml/badge.svg?branch=main)](https://github.com/fastify/fastify/actions/workflows/website.yml) 17 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 18 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/7585/badge)](https://bestpractices.coreinfrastructure.org/projects/7585) 19 | 20 |
21 | 22 |
23 | 24 | [![NPM 25 | version](https://img.shields.io/npm/v/fastify.svg?style=flat)](https://www.npmjs.com/package/fastify) 26 | [![NPM 27 | downloads](https://img.shields.io/npm/dm/fastify.svg?style=flat)](https://www.npmjs.com/package/fastify) 28 | [![Security Responsible 29 | Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/fastify/fastify/blob/main/SECURITY.md) 30 | [![Discord](https://img.shields.io/discord/725613461949906985)](https://discord.gg/fastify) 31 | [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=blue)](https://gitpod.io/#https://github.com/fastify/fastify) 32 | ![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/fastify) 33 | 34 |
35 | 36 |
37 | 38 | # TL;DR 39 | 40 | * [Fastify](https://github.com/fastify/fastify) is a fast and low overhead web framework for Node.js. 41 | * This package shows how fast it is compared to other JS frameworks: these benchmarks do not pretend to represent a real-world scenario, but they give a **good indication of the framework overhead**. 42 | * The benchmarks are run automatically on GitHub actions, which means they run on virtual hardware that can suffer from the "noisy neighbor" effect; this means that the results can vary. 43 | * For metrics (cold-start) see [metrics.md](./METRICS.md) 44 | 45 | # Requirements 46 | 47 | To be included in this list, the framework should captivate users' interest. We have identified the following minimal requirements: 48 | - **Ensure active usage**: a minimum of 500 downloads per week 49 | - **Maintain an active repository** with at least one event (comment, issue, PR) in the last month 50 | - The framework must use the **Node.js** HTTP module 51 | 52 | # Usage 53 | 54 | Clone this repo. Then 55 | 56 | ``` 57 | node ./benchmark [arguments (optional)] 58 | ``` 59 | 60 | #### Arguments 61 | 62 | * `-h`: Help on how to use the tool. 63 | * `bench`: Benchmark one or more modules. 64 | * `compare`: Get comparative data for your benchmarks. 65 | 66 | > Create benchmark before comparing; `benchmark bench` 67 | 68 | > You may also compare all test results, at once, in a single table; `benchmark compare -t` 69 | 70 | > You can also extend the comparison table with percentage values based on fastest result; `benchmark compare -p` 71 | # Benchmarks 72 | 73 | * __Machine:__ linux x64 | 4 vCPUs | 15.6GB Mem 74 | * __Node:__ `v20.19.2` 75 | * __Run:__ Sun Jun 01 2025 02:03:33 GMT+0000 (Coordinated Universal Time) 76 | * __Method:__ `autocannon -c 100 -d 40 -p 10 localhost:3000` (two rounds; one to warm-up, one to measure) 77 | 78 | | | Version | Router | Requests/s | Latency (ms) | Throughput/Mb | 79 | | :-- | --: | --: | :-: | --: | --: | 80 | | node-http | v20.19.2 | ✗ | 49103.2 | 19.88 | 8.76 | 81 | | fastify | 5.3.3 | ✓ | 47056.0 | 20.75 | 8.44 | 82 | | rayo | 1.4.6 | ✓ | 46955.2 | 20.79 | 8.37 | 83 | | connect | 3.7.0 | ✗ | 46831.2 | 20.84 | 8.35 | 84 | | polka | 0.5.2 | ✓ | 46768.0 | 20.87 | 8.34 | 85 | | server-base | 7.1.32 | ✗ | 46647.2 | 20.93 | 8.32 | 86 | | micro | 10.0.1 | ✗ | 46006.4 | 21.23 | 8.20 | 87 | | 0http | 4.2.1 | ✓ | 45880.0 | 21.30 | 8.18 | 88 | | polkadot | 1.0.0 | ✗ | 45716.8 | 21.38 | 8.15 | 89 | | server-base-router | 7.1.32 | ✓ | 45399.2 | 21.53 | 8.10 | 90 | | connect-router | 2.2.0 | ✓ | 43677.6 | 22.38 | 7.79 | 91 | | adonisjs | 7.6.1 | ✓ | 43279.2 | 22.61 | 7.72 | 92 | | micro-route | 2.5.0 | ✓ | 42784.8 | 22.86 | 7.63 | 93 | | h3 | 1.15.3 | ✗ | 42768.8 | 22.88 | 7.63 | 94 | | h3-router | 1.15.3 | ✓ | 41819.2 | 23.42 | 7.46 | 95 | | restana | v5.0.0 | ✓ | 41617.6 | 23.54 | 7.42 | 96 | | hono | 4.7.11 | ✓ | 40141.6 | 24.40 | 6.58 | 97 | | srvx | 0.7.3 | ✗ | 39961.6 | 24.52 | 7.77 | 98 | | whatwg-node-server | 0.10.10 | ✗ | 38295.2 | 25.61 | 6.83 | 99 | | koa | 2.16.1 | ✗ | 37660.6 | 26.05 | 6.72 | 100 | | restify | 11.1.0 | ✓ | 35584.2 | 27.59 | 6.41 | 101 | | take-five | 2.0.0 | ✓ | 35229.0 | 27.87 | 12.67 | 102 | | koa-isomorphic-router | 1.0.1 | ✓ | 35033.8 | 28.04 | 6.25 | 103 | | koa-router | 13.1.0 | ✓ | 33662.4 | 29.21 | 6.00 | 104 | | hapi | 21.4.0 | ✓ | 32034.0 | 30.71 | 5.71 | 105 | | microrouter | 3.1.3 | ✓ | 30264.8 | 32.52 | 5.40 | 106 | | fastify-big-json | 5.3.3 | ✓ | 11811.6 | 84.11 | 135.90 | 107 | | express | 5.1.0 | ✓ | 10037.2 | 99.05 | 1.79 | 108 | | express-with-middlewares | 5.1.0 | ✓ | 9029.8 | 110.11 | 3.36 | 109 | | trpc-router | 11.1.4 | ✓ | 5811.6 | 171.26 | 1.28 | 110 | -------------------------------------------------------------------------------- /benchmark-bench.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | import bench from './lib/bench.js' 3 | import { choices, list } from './lib/packages.js' 4 | const argv = process.argv.slice(2) 5 | 6 | run().catch(err => { 7 | console.error(err) 8 | process.exit(1) 9 | }) 10 | 11 | async function run () { 12 | const options = await getBenchmarkOptions() 13 | const modules = options.all ? choices : await select() 14 | return bench(options, modules) 15 | } 16 | 17 | async function getBenchmarkOptions () { 18 | if (argv.length) return parseArgv() 19 | return inquirer.prompt([ 20 | { 21 | type: 'confirm', 22 | name: 'all', 23 | message: 'Do you want to run all benchmark tests?', 24 | default: false 25 | }, 26 | { 27 | type: 'input', 28 | name: 'connections', 29 | message: 'How many connections do you need?', 30 | default: 100, 31 | validate (value) { 32 | return !Number.isNaN(parseFloat(value)) || 'Please enter a number' 33 | }, 34 | filter: Number 35 | }, 36 | { 37 | type: 'input', 38 | name: 'pipelining', 39 | message: 'How many pipelines do you need?', 40 | default: 10, 41 | validate (value) { 42 | return !Number.isNaN(parseFloat(value)) || 'Please enter a number' 43 | }, 44 | filter: Number 45 | }, 46 | { 47 | type: 'input', 48 | name: 'duration', 49 | message: 'How long should it take?', 50 | default: 40, 51 | validate (value) { 52 | return !Number.isNaN(parseFloat(value)) || 'Please enter a number' 53 | }, 54 | filter: Number 55 | } 56 | ]) 57 | } 58 | 59 | function parseArgv () { 60 | const [all, connections, pipelining, duration] = argv 61 | return { 62 | all: all === 'y', 63 | connections: +connections, 64 | pipelining: +pipelining, 65 | duration: +duration 66 | } 67 | } 68 | 69 | async function select () { 70 | const result = await inquirer.prompt([ 71 | { 72 | type: 'checkbox', 73 | message: 'Select packages', 74 | name: 'list', 75 | choices: [ 76 | new inquirer.Separator(' = The usual ='), 77 | ...list(), 78 | new inquirer.Separator(' = The extras = '), 79 | ...list(true) 80 | ], 81 | validate: function (answer) { 82 | if (answer.length < 1) { 83 | return 'You must choose at least one package.' 84 | } 85 | return true 86 | } 87 | } 88 | ]) 89 | return result.list 90 | } 91 | -------------------------------------------------------------------------------- /benchmark-compare.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { platform, arch, cpus, totalmem } from 'node:os' 4 | import { program } from 'commander' 5 | import inquirer from 'inquirer' 6 | import Table from 'cli-table' 7 | import chalk from 'chalk' 8 | import { join } from 'node:path' 9 | import { readdirSync, readFileSync, writeFileSync } from 'node:fs' 10 | import { info } from './lib/packages.js' 11 | import { compare } from './lib/autocannon.js' 12 | 13 | const resultsPath = join(process.cwd(), 'results') 14 | 15 | program.option('-t, --table', 'print table') 16 | .option('-m --markdown', 'format table for markdown') 17 | .option('-u --update', 'update README.md') 18 | .parse(process.argv) 19 | 20 | const opts = program.opts() 21 | 22 | if (opts.markdown || opts.update) { 23 | chalk.level = 0 24 | } 25 | 26 | if (!getAvailableResults().length) { 27 | console.log(chalk.red('Benchmark to gather some results to compare.')) 28 | } else if (opts.update) { 29 | updateReadme() 30 | } else if (opts.table) { 31 | console.log(compareResults(opts.markdown)) 32 | } else { 33 | compareResultsInteractive() 34 | } 35 | 36 | function getAvailableResults () { 37 | return readdirSync(resultsPath) 38 | .filter((file) => file.match(/(.+)\.json$/)) 39 | .sort() 40 | .map((choice) => choice.replace('.json', '')) 41 | } 42 | 43 | function formatHasRouter (hasRouter) { 44 | return typeof hasRouter === 'string' ? hasRouter : (hasRouter ? '✓' : '✗') 45 | } 46 | 47 | function updateReadme () { 48 | const machineInfo = `${platform()} ${arch()} | ${cpus().length} vCPUs | ${(totalmem() / (1024 ** 3)).toFixed(1)}GB Mem` 49 | const benchmarkMd = `# Benchmarks 50 | 51 | * __Machine:__ ${machineInfo} 52 | * __Node:__ \`${process.version}\` 53 | * __Run:__ ${new Date()} 54 | * __Method:__ \`autocannon -c 100 -d 40 -p 10 localhost:3000\` (two rounds; one to warm-up, one to measure) 55 | 56 | ${compareResults(true)} 57 | ` 58 | const md = readFileSync('README.md', 'utf8') 59 | writeFileSync('README.md', md.split('# Benchmarks', 1)[0] + benchmarkMd, 'utf8') 60 | } 61 | 62 | function compareResults (markdown) { 63 | const tableStyle = !markdown 64 | ? {} 65 | : { 66 | chars: { 67 | top: '', 68 | 'top-left': '', 69 | 'top-mid': '', 70 | 'top-right': '', 71 | bottom: '', 72 | 'bottom-left': '', 73 | 'bottom-mid': '', 74 | 'bottom-right': '', 75 | mid: '', 76 | 'left-mid': '', 77 | 'mid-mid': '', 78 | 'right-mid': '', 79 | left: '|', 80 | right: '|', 81 | middle: '|' 82 | }, 83 | style: { 84 | border: [], 85 | head: [] 86 | } 87 | } 88 | 89 | const table = new Table({ 90 | ...tableStyle, 91 | head: ['', 'Version', 'Router', 'Requests/s', 'Latency (ms)', 'Throughput/Mb'] 92 | }) 93 | 94 | if (markdown) { 95 | table.push([':--', '--:', '--:', ':-:', '--:', '--:']) 96 | } 97 | 98 | const results = getAvailableResults().map(file => { 99 | const content = readFileSync(`${resultsPath}/${file}.json`) 100 | return JSON.parse(content.toString()) 101 | }).sort((a, b) => parseFloat(b.requests.mean) - parseFloat(a.requests.mean)) 102 | 103 | const outputResults = [] 104 | const formatThroughput = throughput => throughput ? (throughput / 1024 / 1024).toFixed(2) : 'N/A' 105 | 106 | for (const result of results) { 107 | const beBold = result.server === 'fastify' 108 | const { hasRouter, version } = info(result.server) || {} 109 | const { 110 | requests: { average: requests }, 111 | latency: { average: latency }, 112 | throughput: { average: throughput } 113 | } = result 114 | 115 | outputResults.push( 116 | { 117 | name: result.server, 118 | version, 119 | hasRouter, 120 | requests: requests ? requests.toFixed(1) : 'N/A', 121 | latency: latency ? latency.toFixed(2) : 'N/A', 122 | throughput: formatThroughput(throughput) 123 | } 124 | ) 125 | 126 | table.push([ 127 | bold(beBold, chalk.blue(result.server)), 128 | bold(beBold, version), 129 | bold(beBold, formatHasRouter(hasRouter)), 130 | bold(beBold, requests ? requests.toFixed(1) : 'N/A'), 131 | bold(beBold, latency ? latency.toFixed(2) : 'N/A'), 132 | bold(beBold, throughput ? (throughput / 1024 / 1024).toFixed(2) : 'N/A') 133 | ]) 134 | } 135 | writeFileSync('benchmark-results.json', JSON.stringify(outputResults), 'utf8') 136 | return table.toString() 137 | } 138 | 139 | async function compareResultsInteractive () { 140 | let choices = getAvailableResults() 141 | 142 | const firstChoice = await inquirer.prompt([{ 143 | type: 'list', 144 | name: 'choice', 145 | message: 'What\'s your first pick?', 146 | choices 147 | }]) 148 | 149 | choices = choices.filter(choice => choice !== firstChoice.choice) 150 | 151 | const secondChoice = await inquirer.prompt([{ 152 | type: 'list', 153 | name: 'choice', 154 | message: 'What\'s your second one?', 155 | choices 156 | }]) 157 | 158 | const [a, b] = [firstChoice.choice, secondChoice.choice] 159 | const result = compare(a, b) 160 | 161 | const fastest = chalk.bold.yellow(result.fastest) 162 | const fastestAverage = chalk.green(result.fastestAverage) 163 | const slowest = chalk.bold.yellow(result.slowest) 164 | const slowestAverage = chalk.green(result.slowestAverage) 165 | const diff = chalk.bold.green(result.diff) 166 | 167 | if (result === true) { 168 | console.log(chalk.green.bold(`${a} and ${b} both are fast!`)) 169 | return 170 | } 171 | 172 | console.log(` 173 | ${chalk.blue('Both are awesome but')} ${fastest} ${chalk.blue('is')} ${diff} ${chalk.blue('faster than')} ${slowest} 174 | • ${fastest} ${chalk.blue('request average is')} ${fastestAverage} 175 | • ${slowest} ${chalk.blue('request average is')} ${slowestAverage}`) 176 | } 177 | 178 | function bold (writeBold, str) { 179 | return writeBold ? chalk.bold(str) : str 180 | } 181 | -------------------------------------------------------------------------------- /benchmark-results.json: -------------------------------------------------------------------------------- 1 | [{"name":"node-http","version":"v20.19.2","requests":"49103.2","latency":"19.88","throughput":"8.76"},{"name":"fastify","version":"5.3.3","hasRouter":true,"requests":"47056.0","latency":"20.75","throughput":"8.44"},{"name":"rayo","version":"1.4.6","hasRouter":true,"requests":"46955.2","latency":"20.79","throughput":"8.37"},{"name":"connect","version":"3.7.0","requests":"46831.2","latency":"20.84","throughput":"8.35"},{"name":"polka","version":"0.5.2","hasRouter":true,"requests":"46768.0","latency":"20.87","throughput":"8.34"},{"name":"server-base","version":"7.1.32","requests":"46647.2","latency":"20.93","throughput":"8.32"},{"name":"micro","version":"10.0.1","requests":"46006.4","latency":"21.23","throughput":"8.20"},{"name":"0http","version":"4.2.1","hasRouter":true,"requests":"45880.0","latency":"21.30","throughput":"8.18"},{"name":"polkadot","version":"1.0.0","hasRouter":false,"requests":"45716.8","latency":"21.38","throughput":"8.15"},{"name":"server-base-router","version":"7.1.32","hasRouter":true,"requests":"45399.2","latency":"21.53","throughput":"8.10"},{"name":"connect-router","version":"2.2.0","hasRouter":true,"requests":"43677.6","latency":"22.38","throughput":"7.79"},{"name":"adonisjs","version":"7.6.1","hasRouter":true,"requests":"43279.2","latency":"22.61","throughput":"7.72"},{"name":"micro-route","version":"2.5.0","hasRouter":true,"requests":"42784.8","latency":"22.86","throughput":"7.63"},{"name":"h3","version":"1.15.3","requests":"42768.8","latency":"22.88","throughput":"7.63"},{"name":"h3-router","version":"1.15.3","hasRouter":true,"requests":"41819.2","latency":"23.42","throughput":"7.46"},{"name":"restana","version":"v5.0.0","hasRouter":true,"requests":"41617.6","latency":"23.54","throughput":"7.42"},{"name":"hono","version":"4.7.11","hasRouter":true,"requests":"40141.6","latency":"24.40","throughput":"6.58"},{"name":"srvx","version":"0.7.3","requests":"39961.6","latency":"24.52","throughput":"7.77"},{"name":"whatwg-node-server","version":"0.10.10","requests":"38295.2","latency":"25.61","throughput":"6.83"},{"name":"koa","version":"2.16.1","requests":"37660.6","latency":"26.05","throughput":"6.72"},{"name":"restify","version":"11.1.0","hasRouter":true,"requests":"35584.2","latency":"27.59","throughput":"6.41"},{"name":"take-five","version":"2.0.0","hasRouter":true,"requests":"35229.0","latency":"27.87","throughput":"12.67"},{"name":"koa-isomorphic-router","version":"1.0.1","hasRouter":true,"requests":"35033.8","latency":"28.04","throughput":"6.25"},{"name":"koa-router","version":"13.1.0","hasRouter":true,"requests":"33662.4","latency":"29.21","throughput":"6.00"},{"name":"hapi","version":"21.4.0","hasRouter":true,"requests":"32034.0","latency":"30.71","throughput":"5.71"},{"name":"microrouter","version":"3.1.3","hasRouter":true,"requests":"30264.8","latency":"32.52","throughput":"5.40"},{"name":"fastify-big-json","version":"5.3.3","hasRouter":true,"requests":"11811.6","latency":"84.11","throughput":"135.90"},{"name":"express","version":"5.1.0","hasRouter":true,"requests":"10037.2","latency":"99.05","throughput":"1.79"},{"name":"express-with-middlewares","version":"5.1.0","hasRouter":true,"requests":"9029.8","latency":"110.11","throughput":"3.36"},{"name":"trpc-router","version":"11.1.4","hasRouter":true,"requests":"5811.6","latency":"171.26","throughput":"1.28"}] -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander' 4 | 5 | program.command('bench', 'Benchmark one, multiple or all modules.', { isDefault: true }) 6 | .command('compare', 'Compare results by module.') 7 | .parse(process.argv) 8 | -------------------------------------------------------------------------------- /benchmarks/0http.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cero = require('0http') 4 | const { router, server } = cero() 5 | 6 | router.get('/', (_req, res) => { 7 | res.setHeader('content-type', 'application/json; charset=utf-8') 8 | res.end(JSON.stringify({ hello: 'world' })) 9 | }) 10 | 11 | server.listen(3000) 12 | -------------------------------------------------------------------------------- /benchmarks/@leizm-web.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Application = require('@leizm/web').Application 4 | 5 | const app = new Application() 6 | 7 | app.use('/', function (ctx) { 8 | ctx.response.json({ hello: 'world' }) 9 | }) 10 | 11 | app.listen(3000) 12 | -------------------------------------------------------------------------------- /benchmarks/adonisjs.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http' 2 | import { defineConfig, Server } from '@adonisjs/http-server' 3 | import { Logger } from '@adonisjs/logger' 4 | import { Emitter } from '@adonisjs/events' 5 | import { Encryption } from '@adonisjs/encryption' 6 | import { Application } from '@adonisjs/application' 7 | 8 | const app = new Application(new URL('./', import.meta.url), { 9 | environment: 'web', 10 | importer: () => {} 11 | }) 12 | 13 | await app.init() 14 | 15 | const encryption = new Encryption({ secret: 'averylongrandom32charslongsecret' }) 16 | 17 | const server = new Server( 18 | app, 19 | encryption, 20 | new Emitter(app), 21 | new Logger({ enabled: false }), 22 | defineConfig({}) 23 | ) 24 | 25 | server.getRouter().get('/', (ctx) => { 26 | return ctx.response.send({ hello: 'world' }) 27 | }) 28 | 29 | await server.boot() 30 | 31 | createServer(server.handle.bind(server)).listen(3000) 32 | -------------------------------------------------------------------------------- /benchmarks/connect-router.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const connect = require('connect') 4 | const router = require('router')() 5 | 6 | const app = connect() 7 | router.get('/', function (_req, res) { 8 | res.setHeader('content-type', 'application/json; charset=utf-8') 9 | res.end(JSON.stringify({ hello: 'world' })) 10 | }) 11 | 12 | app.use(router) 13 | app.listen(3000) 14 | -------------------------------------------------------------------------------- /benchmarks/connect.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const connect = require('connect') 4 | 5 | const app = connect() 6 | app.use(function (_req, res) { 7 | res.setHeader('content-type', 'application/json; charset=utf-8') 8 | res.end(JSON.stringify({ hello: 'world' })) 9 | }) 10 | 11 | app.listen(3000) 12 | -------------------------------------------------------------------------------- /benchmarks/express-with-middlewares.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | 5 | const app = express() 6 | 7 | app.disable('etag') 8 | app.disable('x-powered-by') 9 | 10 | app.use(require('cors')()) 11 | app.use(require('dns-prefetch-control')()) 12 | app.use(require('frameguard')()) 13 | app.use(require('hide-powered-by')()) 14 | app.use(require('hsts')()) 15 | app.use(require('ienoopen')()) 16 | app.use(require('x-xss-protection')()) 17 | 18 | app.get('/', function (_req, res) { 19 | res.json({ hello: 'world' }) 20 | }) 21 | 22 | app.listen(3000) 23 | -------------------------------------------------------------------------------- /benchmarks/express.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | 5 | const app = express() 6 | 7 | app.disable('etag') 8 | app.disable('x-powered-by') 9 | 10 | app.get('/', function (_req, res) { 11 | res.json({ hello: 'world' }) 12 | }) 13 | 14 | app.listen(3000) 15 | -------------------------------------------------------------------------------- /benchmarks/fastify-big-json.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastify = require('fastify')() 4 | 5 | const opts = { 6 | schema: { 7 | response: { 8 | 200: { 9 | type: 'array', 10 | items: { 11 | type: 'object', 12 | properties: { 13 | id: { type: 'integer' }, 14 | title: { type: 'string' }, 15 | employer: { type: 'string' } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | function Employee ({ id = null, title = null, employer = null } = {}) { 24 | this.id = id 25 | this.title = title 26 | this.employer = employer 27 | } 28 | 29 | fastify.get('/', opts, function (_request, reply) { 30 | const jobs = [] 31 | 32 | for (let i = 0; i < 200; i += 1) { 33 | jobs[i] = new Employee({ 34 | id: i, 35 | title: 'Software engineer', 36 | employer: 'Fastify' 37 | }) 38 | } 39 | 40 | reply.send(jobs) 41 | }) 42 | 43 | fastify.listen({ port: 3000, host: '127.0.0.1' }) 44 | -------------------------------------------------------------------------------- /benchmarks/fastify.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastify = require('fastify')() 4 | 5 | const schema = { 6 | schema: { 7 | response: { 8 | 200: { 9 | type: 'object', 10 | properties: { 11 | hello: { 12 | type: 'string' 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | 20 | fastify.get('/', schema, function (_req, reply) { 21 | reply.send({ hello: 'world' }) 22 | }) 23 | 24 | fastify.listen({ port: 3000, host: '127.0.0.1' }) 25 | -------------------------------------------------------------------------------- /benchmarks/h3-router.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createServer } = require('node:http') 4 | const { createApp, toNodeListener, eventHandler, createRouter, setHeader } = require('h3') 5 | 6 | const app = createApp() 7 | 8 | const router = createRouter() 9 | .get('/', eventHandler((ev) => { 10 | // Unfortunatly, we need to set the content-type manually 11 | // to level the paying field 12 | setHeader(ev, 'content-type', 'application/json; charset=utf-8') 13 | return { hello: 'world' } 14 | })) 15 | 16 | app.use(router) 17 | 18 | createServer(toNodeListener(app)).listen(process.env.PORT || 3000) 19 | -------------------------------------------------------------------------------- /benchmarks/h3.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createServer } = require('node:http') 4 | const { createApp, toNodeListener, eventHandler, setHeader } = require('h3') 5 | 6 | const app = createApp() 7 | app.use('/', eventHandler((ev) => { 8 | // Unfortunatly, we need to set the content-type manually 9 | // to level the paying field 10 | setHeader(ev, 'content-type', 'application/json; charset=utf-8') 11 | return { hello: 'world' } 12 | })) 13 | 14 | createServer(toNodeListener(app)).listen(process.env.PORT || 3000) 15 | -------------------------------------------------------------------------------- /benchmarks/hapi.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Hapi = require('@hapi/hapi') 4 | 5 | async function start () { 6 | const server = Hapi.server({ port: 3000, debug: false }) 7 | 8 | server.route({ 9 | method: 'GET', 10 | path: '/', 11 | config: { 12 | cache: false, 13 | response: { 14 | ranges: false 15 | }, 16 | state: { parse: false } 17 | }, 18 | handler: function () { 19 | return { hello: 'world' } 20 | } 21 | }) 22 | 23 | await server.start() 24 | } 25 | 26 | start() 27 | -------------------------------------------------------------------------------- /benchmarks/hono.mjs: -------------------------------------------------------------------------------- 1 | import { serve } from '@hono/node-server' 2 | import { Hono } from 'hono' 3 | 4 | const app = new Hono() 5 | app.get('/', (c) => c.json({ hello: 'world' })) 6 | 7 | serve(app) 8 | -------------------------------------------------------------------------------- /benchmarks/koa-isomorphic-router.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Koa = require('koa') 4 | const Router = require('koa-isomorphic-router') 5 | 6 | const app = new Koa() 7 | const router = new Router() 8 | 9 | router.get('/', function (ctx) { 10 | ctx.body = { hello: 'world' } 11 | }) 12 | 13 | app.use(router.routes()) 14 | app.listen(3000) 15 | -------------------------------------------------------------------------------- /benchmarks/koa-router.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Koa = require('koa') 4 | const Router = require('@koa/router') 5 | 6 | const app = new Koa() 7 | const router = new Router() 8 | 9 | router.get('/', async function (ctx) { 10 | ctx.body = { hello: 'world' } 11 | }) 12 | 13 | app.use(router.routes()) 14 | app.listen(3000) 15 | -------------------------------------------------------------------------------- /benchmarks/koa.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Koa = require('koa') 4 | const app = new Koa() 5 | 6 | app.use(ctx => { 7 | ctx.body = { hello: 'world' } 8 | }) 9 | 10 | const _server = app.listen(3000) 11 | 12 | process.on('SIGINT', () => { 13 | _server.close() 14 | }) 15 | -------------------------------------------------------------------------------- /benchmarks/micro-route.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('node:http') 4 | const { send, serve } = require('micro') 5 | const dispatch = require('micro-route/dispatch') 6 | 7 | const handler = (_req, res) => send(res, 200, { hello: 'world' }) 8 | 9 | const server = new http.Server(serve(dispatch('/', 'GET', handler))) 10 | 11 | server.listen(3000) 12 | -------------------------------------------------------------------------------- /benchmarks/micro.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('node:http') 4 | const { serve } = require('micro') 5 | 6 | const server = new http.Server( 7 | serve(async function () { 8 | return { hello: 'world' } 9 | }) 10 | ) 11 | 12 | server.listen(3000) 13 | -------------------------------------------------------------------------------- /benchmarks/microrouter.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('node:http') 4 | const { serve, send } = require('micro') 5 | const { router, get } = require('microrouter') 6 | 7 | const hello = async function (_req, res) { 8 | return send(res, 200, { hello: 'world' }) 9 | } 10 | const server = new http.Server(serve(router(get('/', hello)))) 11 | 12 | server.listen(3000) 13 | -------------------------------------------------------------------------------- /benchmarks/node-http.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const server = require('node:http').createServer(function (_req, res) { 4 | res.setHeader('content-type', 'application/json; charset=utf-8') 5 | res.end(JSON.stringify({ hello: 'world' })) 6 | }) 7 | 8 | server.listen(3000) 9 | -------------------------------------------------------------------------------- /benchmarks/polka.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const polka = require('polka') 4 | 5 | const app = polka() 6 | 7 | app.get('/', (_req, res) => { 8 | res.setHeader('content-type', 'application/json; charset=utf-8') 9 | res.end(JSON.stringify({ hello: 'world' })) 10 | }) 11 | 12 | app.listen(3000) 13 | -------------------------------------------------------------------------------- /benchmarks/polkadot.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const polkadot = require('polkadot') 4 | 5 | polkadot(function (_req, res) { 6 | res.setHeader('content-type', 'application/json; charset=utf-8') 7 | res.end(JSON.stringify({ hello: 'world' })) 8 | }).listen(3000) 9 | -------------------------------------------------------------------------------- /benchmarks/rayo.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | async function run () { 4 | const { default: rayo } = await import('rayo') 5 | 6 | const app = rayo({ port: 3000 }) 7 | 8 | app.get('/', (_req, res) => { 9 | res.setHeader('content-type', 'application/json; charset=utf-8') 10 | res.end(JSON.stringify({ hello: 'world' })) 11 | }) 12 | 13 | app.start() 14 | } 15 | 16 | run() 17 | -------------------------------------------------------------------------------- /benchmarks/restana.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const restana = require('restana') 4 | 5 | const app = restana() 6 | 7 | app.get('/', (_req, res) => { 8 | res.send({ hello: 'world' }) 9 | }) 10 | 11 | app.start(3000) 12 | -------------------------------------------------------------------------------- /benchmarks/restify.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const restify = require('restify') 4 | 5 | const server = restify.createServer() 6 | server.get('/', function (_req, res, next) { 7 | res.send({ hello: 'world' }) 8 | return next() 9 | }) 10 | 11 | server.listen(3000, function () {}) 12 | -------------------------------------------------------------------------------- /benchmarks/server-base-router.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('node:http') 4 | .createServer( 5 | require('server-base-router')({ 6 | '@setup' (ctx) { 7 | ctx.middlewareFunctions = [] 8 | }, 9 | '/': { 10 | get (_req, res) { 11 | res.setHeader('content-type', 'application/json; charset=utf-8') 12 | res.json({ hello: 'world' }) 13 | } 14 | } 15 | }) 16 | ) 17 | .listen(3000) 18 | -------------------------------------------------------------------------------- /benchmarks/server-base.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('server-base')({ 4 | '@setup' (ctx) { 5 | ctx.middlewareFunctions = [] 6 | }, 7 | '/': { 8 | get (_req, res) { 9 | res.setHeader('content-type', 'application/json; charset=utf-8') 10 | res.json({ hello: 'world' }) 11 | } 12 | } 13 | }).start(3000) 14 | -------------------------------------------------------------------------------- /benchmarks/srvx.mjs: -------------------------------------------------------------------------------- 1 | import { FastResponse, serve } from 'srvx' 2 | 3 | serve({ 4 | fetch: () => FastResponse.json({ hello: 'world' }, { 5 | headers: { 6 | 'content-type': 'application/json; charset=utf-8' 7 | } 8 | }), 9 | port: 3000 10 | }) 11 | -------------------------------------------------------------------------------- /benchmarks/take-five.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Five = require('take-five') 4 | 5 | const server = new Five() 6 | 7 | server.get('/', function (_req, _res, ctx) { 8 | return ctx.send({ hello: 'world' }) 9 | }) 10 | 11 | server.listen(3000) 12 | -------------------------------------------------------------------------------- /benchmarks/tinyhttp.mjs: -------------------------------------------------------------------------------- 1 | import { App } from '@tinyhttp/app' 2 | 3 | const app = new App() 4 | 5 | app.get('/', (_req, res) => { 6 | res.send('Hello World!') 7 | }) 8 | 9 | app.listen(3000) 10 | -------------------------------------------------------------------------------- /benchmarks/trpc-router.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { initTRPC } = require('@trpc/server') 4 | const { fastifyTRPCPlugin } = require('@trpc/server/adapters/fastify') 5 | const fastify = require('fastify')() 6 | 7 | // https://trpc.io/docs/v11/router 8 | const t = initTRPC.create() 9 | const appRouter = t.router({ 10 | '': t.procedure.query(() => { 11 | return { hello: 'world' } 12 | }) 13 | }) 14 | 15 | fastify.register(fastifyTRPCPlugin, { 16 | prefix: '', 17 | trpcOptions: { router: appRouter, createContext: () => {} } 18 | }) 19 | 20 | // Route URL is composed by prefix + query() first string param. 21 | // In this benchmark, assigning an empty string to both of them is a way for exposing URL "/". 22 | // A more realistic case would be having prefix="/trpc" and query('tasks'), 23 | // which would expose the URL "/trpc/tasks" 24 | fastify.listen({ port: 3000, host: '127.0.0.1' }) 25 | -------------------------------------------------------------------------------- /benchmarks/whatwg-node-server.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http' 2 | import { createServerAdapter, Response } from '@whatwg-node/server' 3 | 4 | createServer( 5 | createServerAdapter(() => Response.json({ hello: 'world' })) 6 | ).listen(3000) 7 | -------------------------------------------------------------------------------- /lib/autocannon.js: -------------------------------------------------------------------------------- 1 | import autocannon from 'autocannon' 2 | import { writeFile as _writeFile, mkdir as _mkdir, access as _access } from 'node:fs' 3 | import compare from 'autocannon-compare' 4 | import { join } from 'node:path' 5 | import { promisify } from 'node:util' 6 | import { createRequire } from 'node:module' 7 | 8 | const writeFile = promisify(_writeFile) 9 | const mkdir = promisify(_mkdir) 10 | const access = promisify(_access) 11 | const require = createRequire(import.meta.url) 12 | 13 | const resultsDirectory = join(process.cwd(), 'results') 14 | 15 | const run = (opts = {}) => new Promise((resolve, reject) => { 16 | opts.url = 'http://127.0.0.1:3000' 17 | autocannon(opts, (err, result) => { 18 | if (err) { 19 | reject(err) 20 | } else { 21 | resolve(result) 22 | } 23 | }) 24 | }) 25 | 26 | const writeResult = async (handler, result) => { 27 | try { 28 | await access(resultsDirectory) 29 | } catch { 30 | await mkdir(resultsDirectory) 31 | } 32 | 33 | result.server = handler 34 | 35 | const dest = join(resultsDirectory, `${handler}.json`) 36 | return writeFile(dest, JSON.stringify(result)) 37 | } 38 | 39 | export async function fire (opts, handler, save) { 40 | const result = await run(opts) 41 | return save ? writeResult(handler, result) : null 42 | } 43 | 44 | const _compare = (a, b) => { 45 | const resA = require(`${resultsDirectory}/${a}.json`) 46 | const resB = require(`${resultsDirectory}/${b}.json`) 47 | const comp = compare(resA, resB) 48 | if (comp.equal) { 49 | return true 50 | } else if (comp.aWins) { 51 | return { 52 | diff: comp.requests.difference, 53 | fastest: a, 54 | slowest: b, 55 | fastestAverage: resA.requests.average, 56 | slowestAverage: resB.requests.average 57 | } 58 | } 59 | return { 60 | diff: compare(resB, resA).requests.difference, 61 | fastest: b, 62 | slowest: a, 63 | fastestAverage: resB.requests.average, 64 | slowestAverage: resA.requests.average 65 | } 66 | } 67 | export { _compare as compare } 68 | -------------------------------------------------------------------------------- /lib/bench.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { access } from 'node:fs/promises' 4 | import { fork } from 'node:child_process' 5 | import ora from 'ora' 6 | import { join } from 'node:path' 7 | import { fire } from './autocannon.js' 8 | import { fileURLToPath } from 'node:url' 9 | import assert from 'node:assert' 10 | 11 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 12 | 13 | const doBench = async (opts, handler) => { 14 | const spinner = ora(`Started ${handler}`).start() 15 | let forked 16 | try { 17 | await access(join(__dirname, '..', 'benchmarks', handler + '.cjs')) 18 | forked = fork(join(__dirname, '..', 'benchmarks', handler + '.cjs')) 19 | } catch { 20 | forked = fork(join(__dirname, '..', 'benchmarks', handler + '.mjs')) 21 | } 22 | 23 | try { 24 | spinner.color = 'magenta' 25 | spinner.text = `Warming ${handler}` 26 | await fire(opts, handler, false) 27 | } catch (error) { 28 | return console.log(error) 29 | } finally { 30 | spinner.color = 'yellow' 31 | spinner.text = `Working ${handler}` 32 | } 33 | 34 | try { 35 | await fire(opts, handler, true) 36 | assert.ok(forked.kill('SIGINT')) 37 | spinner.text = `Results saved for ${handler}` 38 | spinner.succeed() 39 | return true 40 | } catch (error) { 41 | return console.log(error) 42 | } 43 | } 44 | 45 | let index = 0 46 | const start = async (opts, list) => { 47 | if (list.length === index) { 48 | return true 49 | } 50 | 51 | try { 52 | await doBench(opts, list[index]) 53 | index += 1 54 | return start(opts, list) 55 | } catch (error) { 56 | return console.log(error) 57 | } 58 | } 59 | 60 | export default start 61 | -------------------------------------------------------------------------------- /lib/packages.js: -------------------------------------------------------------------------------- 1 | import pkgJson from '../package.json' with { type: 'json' } 2 | import { createRequire } from 'node:module'; 3 | import { resolve } from 'node:path'; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | const packages = { 8 | '0http': { hasRouter: true, package: '0http' }, 9 | 'adonisjs': { hasRouter: true, package: '@adonisjs/http-server' }, 10 | connect: {}, 11 | 'connect-router': { extra: true, package: 'router', hasRouter: true }, 12 | express: { hasRouter: true }, 13 | 'express-with-middlewares': { extra: true, package: 'express', hasRouter: true }, 14 | fastify: { checked: true, hasRouter: true }, 15 | 'fastify-big-json': { extra: true, package: 'fastify', hasRouter: true }, 16 | h3: { package: 'h3' }, 17 | 'h3-router': { hasRouter: true, package: 'h3' }, 18 | hapi: { hasRouter: true, package: '@hapi/hapi' }, 19 | hono: { hasRouter: true, package: 'hono' }, 20 | koa: {}, 21 | 'koa-isomorphic-router': { extra: true, hasRouter: true }, 22 | 'koa-router': { extra: true, hasRouter: true, package: '@koa/router' }, 23 | micro: { extra: true }, 24 | 'micro-route': { extra: true, hasRouter: true }, 25 | microrouter: { extra: true, hasRouter: true }, 26 | 'node-http': { version: process.version }, 27 | polka: { hasRouter: true }, 28 | polkadot: { hasRouter: false }, 29 | rayo: { hasRouter: true }, 30 | restana: { hasRouter: true, package: 'restana' }, 31 | restify: { hasRouter: true }, 32 | 'server-base': {}, 33 | 'server-base-router': { hasRouter: true }, 34 | 'srvx': { package: 'srvx' }, 35 | 'take-five': { hasRouter: true }, 36 | 'trpc-router': { extra: true, hasRouter: true, package: '@trpc/server' }, 37 | 'whatwg-node-server': { package: '@whatwg-node/server' }, 38 | } 39 | 40 | const _choices = [] 41 | Object.keys(packages).forEach(pkg => { 42 | if (!packages[pkg].version) { 43 | const module = pkgJson.dependencies[pkg] ? pkg : packages[pkg].package 44 | const version = require(resolve(`node_modules/${module}/package.json`)).version 45 | packages[pkg].version = version 46 | } 47 | _choices.push(pkg) 48 | }) 49 | 50 | export const choices = _choices.sort() 51 | export function list(extra = false) { 52 | return _choices 53 | .map(c => { 54 | return extra === !!packages[c].extra 55 | ? Object.assign({}, packages[c], { name: c }) 56 | : null 57 | }) 58 | .filter(c => c) 59 | } 60 | export function info(module) { 61 | return packages[module] 62 | } 63 | -------------------------------------------------------------------------------- /metrics/.gitignore: -------------------------------------------------------------------------------- 1 | *.js.txt 2 | -------------------------------------------------------------------------------- /metrics/process-results.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('node:fs') 4 | const path = require('node:path') 5 | const os = require('node:os') 6 | 7 | function readableHRTimeMs (diff) { 8 | return (diff[0] * 1e9 + diff[1]) / 1000000 9 | } 10 | 11 | function updateReadme (startupResults) { 12 | const machineInfo = `${os.platform()} ${os.arch()} | ${os.cpus().length} vCPUs | ${(os.totalmem() / (1024 ** 3)).toFixed(1)}GB Mem` 13 | const benchmarkMd = `# Metrics 14 | * __Machine:__ ${machineInfo} 15 | * __Node:__ \`${process.version}\` 16 | * __Run:__ ${new Date()} 17 | * __Method:__ \`npm run metrics\` (samples: 5) 18 | * __startup:__ time elapsed to setup the application 19 | * __listen:__ time elapsed until the http server is ready to accept requests (cold start) 20 | ${startupResults} 21 | ` 22 | const md = fs.readFileSync('METRICS.md', 'utf8') 23 | fs.writeFileSync('METRICS.md', md.split('# Metrics', 1)[0] + benchmarkMd, 'utf8') 24 | } 25 | 26 | const results = fs.readdirSync(__dirname).filter((x) => x.endsWith('.txt')) 27 | 28 | let md = ` 29 | | | startup(ms) | listen(ms) | 30 | |-| - | - |` 31 | 32 | for (const r of results) { 33 | const data = fs.readFileSync(path.join(__dirname, r), { encoding: 'utf-8' }) 34 | const lines = data.split('\n').filter(Boolean) 35 | const temp = { 36 | startup: 0, 37 | listen: 0 38 | } 39 | lines.forEach((x) => { 40 | const [startup, listen] = x.split('|') 41 | temp.startup += readableHRTimeMs(startup.split(',').map(x => parseInt(x))) 42 | temp.listen += readableHRTimeMs(listen.split(',').map(x => parseInt(x))) 43 | }) 44 | md += `\n| ${r.replace('.txt', '')} | ${(temp.startup / lines.length).toFixed(2)} | ${(temp.listen / lines.length).toFixed(2)} |` 45 | } 46 | 47 | if (process.argv.length >= 3 && process.argv[2] === '-u') { 48 | console.debug('Updating METRICS...') 49 | updateReadme(md) 50 | } 51 | console.log(md) 52 | -------------------------------------------------------------------------------- /metrics/startup-listen.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const start = process.hrtime() 4 | 5 | const fastify = require('fastify') 6 | const server = fastify() 7 | 8 | const loadingTime = process.hrtime(start) 9 | 10 | server.listen({ port: 3000 }, () => { 11 | const listenTime = process.hrtime(start) 12 | require('node:fs').writeFileSync(`${__filename}.txt`, `${loadingTime} | ${listenTime}\n`, { encoding: 'utf-8', flag: 'a' }) 13 | server.close() 14 | }) 15 | -------------------------------------------------------------------------------- /metrics/startup-routes-schema.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const start = process.hrtime() 4 | 5 | const fastify = require('fastify') 6 | const server = fastify() 7 | 8 | const routes = process.env.routes || 0 9 | 10 | for (let i = 0; i < routes; ++i) { 11 | server.get( 12 | `/${i}`, 13 | { 14 | schema: { 15 | querystring: { 16 | [i]: { type: 'string' }, 17 | excitement: { type: 'integer' } 18 | } 19 | } 20 | }, 21 | (_req, reply) => { 22 | reply.send({}) 23 | } 24 | ) 25 | } 26 | 27 | const loadingTime = process.hrtime(start) 28 | server.listen({ port: 0 }, () => { 29 | const listenTime = process.hrtime(start) 30 | const path = require('node:path') 31 | require('node:fs').writeFileSync(path.join(__dirname, `${routes}-${path.basename(__filename)}.txt`), `${loadingTime} | ${listenTime}\n`, { encoding: 'utf-8', flag: 'a' }) 32 | server.close() 33 | }) 34 | -------------------------------------------------------------------------------- /metrics/startup-routes.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const start = process.hrtime() 4 | 5 | const fastify = require('fastify') 6 | const server = fastify() 7 | 8 | const routes = process.env.routes || 0 9 | 10 | for (let i = 0; i < routes; ++i) { 11 | server.get( 12 | `/${i}`, 13 | (_req, reply) => { 14 | reply.send({}) 15 | } 16 | ) 17 | } 18 | const loadingTime = process.hrtime(start) 19 | 20 | server.listen({ port: 0 }, () => { 21 | const listenTime = process.hrtime(start) 22 | const path = require('node:path') 23 | require('node:fs').writeFileSync(path.join(__dirname, `${routes}-${path.basename(__filename)}.txt`), `${loadingTime} | ${listenTime}\n`, { encoding: 'utf-8', flag: 'a' }) 24 | server.close() 25 | }) 26 | -------------------------------------------------------------------------------- /metrics/startup.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Worker } = require('node:worker_threads') 4 | const path = require('node:path') 5 | 6 | const minSamples = 5 7 | 8 | const runSample = (cb) => { 9 | return async () => { 10 | for (let i = 0; i < minSamples; ++i) { 11 | await cb() 12 | } 13 | } 14 | } 15 | 16 | const measureStartupListen = runSample(() => { 17 | return new Promise((resolve) => { 18 | new Worker(path.join(__dirname, './startup-listen.cjs')) 19 | .on('exit', resolve) 20 | }) 21 | }) 22 | 23 | const measureStartupNRoutes = runSample(async () => { 24 | for (let n = 1; n <= 10000; n *= 10) { 25 | await new Promise((resolve) => { 26 | new Worker( 27 | path.join(__dirname, './startup-routes.cjs'), 28 | { 29 | env: { 30 | routes: n 31 | } 32 | } 33 | ).on('exit', resolve) 34 | }) 35 | } 36 | }) 37 | 38 | const measureStartupNSchemaRoutes = runSample(async () => { 39 | for (let n = 1; n <= 10000; n *= 10) { 40 | await new Promise((resolve) => { 41 | new Worker( 42 | path.join(__dirname, './startup-routes-schema.cjs'), 43 | { 44 | env: { 45 | routes: n 46 | } 47 | } 48 | ).on('exit', resolve) 49 | }) 50 | } 51 | }) 52 | 53 | measureStartupListen() 54 | .then(measureStartupNRoutes) 55 | .then(measureStartupNSchemaRoutes) 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-benchmarks", 3 | "version": "1.0.0", 4 | "description": "Benchmarks for Fastify, a fast and low-overhead web framework.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node benchmark.js", 9 | "compare": "node benchmark.js compare --", 10 | "test": "standard | snazzy", 11 | "standard": "standard | snazzy", 12 | "metrics:run": "node metrics/startup.cjs", 13 | "metrics:summary": "node metrics/process-results.cjs -u" 14 | }, 15 | "bin": { 16 | "benchmark": "benchmark.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/fastify/benchmarks.git" 21 | }, 22 | "author": "Çağatay Çalı", 23 | "contributors": [ 24 | { 25 | "name": "Stefan Aichholzer", 26 | "email": "theaichholzer@gmail.com", 27 | "url": "https://github.com/aichholzer" 28 | } 29 | ], 30 | "standard": { 31 | "ignore": [ 32 | "lib/packages.js" 33 | ] 34 | }, 35 | "license": "MIT", 36 | "dependencies": { 37 | "@adonisjs/application": "^8.3.1", 38 | "@adonisjs/encryption": "^6.0.2", 39 | "@adonisjs/events": "^9.0.2", 40 | "@adonisjs/http-server": "^7.2.3", 41 | "@adonisjs/logger": "^6.0.3", 42 | "@hapi/hapi": "^21.1.0", 43 | "@hono/node-server": "^1.3.0", 44 | "@koa/router": "^13.1.0", 45 | "@leizm/web": "^2.7.3", 46 | "@tinyhttp/app": "^2.2.1", 47 | "@trpc/server": "^11.1.0", 48 | "@whatwg-node/server": "^0.10.6", 49 | "0http": "^4.0.0", 50 | "autocannon": "^8.0.0", 51 | "autocannon-compare": "^0.4.0", 52 | "benchmark": "^2.1.4", 53 | "chalk": "^5.2.0", 54 | "cli-table": "^0.3.11", 55 | "commander": "^13.1.0", 56 | "connect": "^3.7.0", 57 | "cors": "^2.8.5", 58 | "dns-prefetch-control": "^0.3.0", 59 | "express": "^5.0.0", 60 | "fastify": "^5.0.0", 61 | "frameguard": "^4.0.0", 62 | "h3": "^1.10.0", 63 | "hide-powered-by": "^1.1.0", 64 | "hono": "^4.0.1", 65 | "hsts": "^2.2.0", 66 | "ienoopen": "^1.1.1", 67 | "inquirer": "^12.1.0", 68 | "koa": "^2.14.1", 69 | "koa-isomorphic-router": "^1.0.1", 70 | "micro": "^10.0.1", 71 | "micro-route": "^2.5.0", 72 | "microrouter": "^3.1.3", 73 | "ora": "^8.1.1", 74 | "polka": "^0.5.2", 75 | "polkadot": "^1.0.0", 76 | "rayo": "^1.4.5", 77 | "restana": "^5.0.0", 78 | "restify": "^11.0.0", 79 | "router": "^2.2.0", 80 | "server-base": "^7.1.32", 81 | "server-base-router": "^7.1.32", 82 | "srvx": "^0.7.1", 83 | "take-five": "^2.0.0", 84 | "x-xss-protection": "^2.0.0" 85 | }, 86 | "devDependencies": { 87 | "snazzy": "^9.0.0", 88 | "standard": "^17.0.0" 89 | } 90 | } 91 | --------------------------------------------------------------------------------