├── .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 |
9 |
10 |
11 |
12 | [](https://github.com/fastify/fastify/actions/workflows/ci.yml)
13 | [](https://github.com/fastify/fastify/actions/workflows/package-manager-ci.yml)
15 | [](https://github.com/fastify/fastify/actions/workflows/website.yml)
17 | [](https://github.com/neostandard/neostandard)
18 | [](https://bestpractices.coreinfrastructure.org/projects/7585)
19 |
20 |
21 |
22 |
23 |
24 | [](https://www.npmjs.com/package/fastify)
26 | [](https://www.npmjs.com/package/fastify)
28 | [](https://github.com/fastify/fastify/blob/main/SECURITY.md)
30 | [](https://discord.gg/fastify)
31 | [](https://gitpod.io/#https://github.com/fastify/fastify)
32 | 
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 |
--------------------------------------------------------------------------------