diff --git a/.github/scripts/run-benchmark-client.sh b/.github/scripts/run-benchmark-client.sh
new file mode 100644
index 000000000..d0d4dace0
--- /dev/null
+++ b/.github/scripts/run-benchmark-client.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+set -euo pipefail
+
+# benchmark over ssh
+#
+# MagicOnion Client
+# $ ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_ed25519 azure-user@4.215.237.255 'bash -s -- --args "-u http://${BENCHMARK_SERVER_NAME}:5000 -s streaminghub --channels 1 --streams 1"' < ./scripts/run-benchmark.sh
+# $ echo $?
+
+function usage {
+    echo "usage: $(basename $0) --args <string> [options]"
+    echo "Required:"
+    echo "  --args          string      Arguments to pass when running the built binary (default: \"\")"
+    echo "Options:"
+    echo "  --help                      Show this help message"
+}
+
+while [ $# -gt 0 ]; do
+  case $1 in
+    # required
+    --args) _ARGS=$2; shift 2; ;;
+    # optional
+    --help) usage; exit 1; ;;
+    *) shift ;;
+  esac
+done
+
+function print() {
+  echo ""
+  echo "$*"
+}
+
+# parameter setup
+repo="MagicOnion"
+build_config="Release"
+args="${_ARGS:=""}"
+build_csproj="perf/BenchmarkApp/PerformanceTest.Client/PerformanceTest.Client.csproj"
+env_settings=""
+
+binary_name=$(basename "$(dirname "$build_csproj")")
+publish_dir="artifacts/$binary_name"
+clone_path="$HOME/github/$repo"
+output_dir="$clone_path/$publish_dir"
+full_process_path="$output_dir/$binary_name"
+
+# show machine name
+print "MACHINE_NAME: $(hostname)"
+
+# is dotnet installed?
+print "# Show installed dotnet sdk versions"
+echo "dotnet sdk versions (list): $(dotnet --list-sdks)"
+echo "dotnet sdk version (default): $(dotnet --version)"
+
+# setup env
+print "# Setup environment"
+IFS=';' read -ra env_array <<< "$env_settings"
+for item in "${env_array[@]}"; do
+  if [ -n "$item" ]; then
+    export "$item"
+  fi
+done
+
+# dotnet publish
+print "# dotnet publish $build_csproj"
+pushd "$clone_path"
+  print "  ## list current files under $(pwd)"
+  ls -l
+
+  print "  ## dotnet publish $build_csproj"
+  dotnet publish -c "$build_config" -p:PublishSingleFile=true --runtime linux-x64 --self-contained false "$build_csproj" -o "$publish_dir"
+
+  print "  ## list published files under $publish_dir"
+  ls "$publish_dir"
+
+  print "  ## add +x permission to published file $full_process_path"
+  chmod +x "$full_process_path"
+popd
+
+# process check
+print "# Checking process $binary_name already runnning, kill if exists"
+ps -eo pid,cmd | while read -r pid cmd; do
+  if echo "$cmd" | grep -E "^./$binary_name" >/dev/null 2>&1; then
+    echo "Found & killing process $pid ($cmd)"
+    kill "$pid"
+  fi
+done
+
+# run dotnet app
+print "# Run $full_process_path $args"
+pushd "$output_dir"
+  # run foreground
+  "./$binary_name" $args
+popd
diff --git a/.github/scripts/run-benchmark-server.sh b/.github/scripts/run-benchmark-server.sh
new file mode 100644
index 000000000..271388b7b
--- /dev/null
+++ b/.github/scripts/run-benchmark-server.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+set -euo pipefail
+
+# benchmark over ssh
+#
+# MagicOnion Server
+# ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_ed25519 azure-user@4.215.238.2 'bash -s -- ' < ./scripts/run-server
+# $ echo $?
+
+function usage {
+    echo "usage: $(basename $0) [options]"
+    echo "Options:"
+    echo "  --help                      Show this help message"
+}
+
+while [ $# -gt 0 ]; do
+  case $1 in
+    --help) usage; exit 1; ;;
+    *) shift ;;
+  esac
+done
+
+function print() {
+  echo ""
+  echo "$*"
+}
+
+# parameter setup
+repo="MagicOnion"
+build_config="Release"
+build_csproj="perf/BenchmarkApp/PerformanceTest.Server/PerformanceTest.Server.csproj"
+env_settings=""
+
+binary_name=$(basename "$(dirname "$build_csproj")")
+publish_dir="artifacts/$binary_name"
+clone_path="$HOME/github/$repo"
+output_dir="$clone_path/$publish_dir"
+full_process_path="$output_dir/$binary_name"
+
+stdoutfile="stdout.log"
+stderrfile="stderr.log"
+
+# show machine name
+print "MACHINE_NAME: $(hostname)"
+
+# is dotnet installed?
+print "# Show installed dotnet sdk versions"
+echo "dotnet sdk versions (list): $(dotnet --list-sdks)"
+echo "dotnet sdk version (default): $(dotnet --version)"
+
+# setup env
+print "# Setup environment"
+IFS=';' read -ra env_array <<< "$env_settings"
+for item in "${env_array[@]}"; do
+  if [ -n "$item" ]; then
+    export "$item"
+  fi
+done
+
+# process check
+print "# Checking process $binary_name already runnning, kill if exists"
+ps -eo pid,cmd | while read -r pid cmd; do
+  if echo "$cmd" | grep -E "^./$binary_name" >/dev/null 2>&1; then
+    echo "Found & killing process $pid ($cmd)"
+    kill "$pid"
+  fi
+done
+
+# dotnet publish
+print "# dotnet publish $build_csproj"
+pushd "$clone_path"
+  print "  ## list current files under $(pwd)"
+  ls -l
+
+  print "  ## dotnet publish $build_csproj"
+  dotnet publish -c "$build_config" -p:PublishSingleFile=true --runtime linux-x64 --self-contained false "$build_csproj" -o "$publish_dir"
+
+  print "  ## list published files under $publish_dir"
+  ls "$publish_dir"
+
+  print "  ## add +x permission to published file $full_process_path"
+  chmod +x "$full_process_path"
+popd
+
+# run dotnet app
+print "# Run $full_process_path"
+pushd "$output_dir"
+  # run background https://stackoverflow.com/questions/29142/getting-ssh-to-execute-a-command-in-the-background-on-target-machine
+  nohup "./$binary_name" > "${stdoutfile}" 2> "${stderrfile}" < /dev/null &
+
+  # wait 10s will be enough to start the server or not
+  sleep 10s
+
+  # output stdout
+  cat "${stdoutfile}"
+
+  # output stderr
+  if [[ "$(stat -c%s "$stderrfile")" -ne "0" ]]; then
+    echo "Error found when running the server."
+    cat "${stderrfile}"
+    exit 1
+  fi
+popd
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 000000000..ef00c6be7
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,44 @@
+name: Benchmark
+
+on:
+  issue_comment:
+    types: [created]
+  workflow_dispatch:
+
+permissions:
+  contents: read
+  id-token: write
+
+jobs:
+  prepare:
+    outputs:
+      branch: ${{ steps.get-branch.outputs.name }}
+    runs-on: ubuntu-latest
+    timeout-minutes: 5
+    steps:
+      - uses: actions/checkout@v4
+      - name: Get branch name
+        id: get-branch
+        run: |
+          if [[ "${{ github.event_name}}" == "issue_comment" && "${{ github.event.issue.pull_request.html_url }}" != "" ]]; then
+            # issue_comment (pull_request)
+            branch=$(gh pr view "${{ github.event.issue.pull_request.html_url }}"  --json headRefName | jq -r ".headRefName")
+            echo "name=${branch}" | tee -a "$GITHUB_OUTPUT"
+          else
+            # workflow_dispatch or issue_comment (issue)
+            echo "name=${{ github.ref_name }}" | tee -a "$GITHUB_OUTPUT"
+          fi
+
+  # run benchmark
+  benchmark:
+    needs: [prepare]
+    uses: Cysharp/Actions/.github/workflows/benchmark.yaml@main
+    with:
+      branch: ${{ needs.prepare.outputs.branch }}
+      dotnet-version: "8.0"
+      environment: benchmark
+      client-benchmark-script-path: ".github/scripts/run-benchmark-client.sh"
+      client-benchmark-script-args: "--args \"-u http://${BENCHMARK_SERVER_NAME}:5000 -s streaminghub --channels 1 --streams 1\""
+      server-benchmark-script-path: ".github/scripts/run-benchmark-server.sh"
+      server-benchmark-script-args: ""
+    secrets: inherit
diff --git a/perf/BenchmarkApp/PerformanceTest.Server/Properties/launchSettings.json b/perf/BenchmarkApp/PerformanceTest.Server/Properties/launchSettings.json
index 2151c9d5c..0d2e73873 100644
--- a/perf/BenchmarkApp/PerformanceTest.Server/Properties/launchSettings.json
+++ b/perf/BenchmarkApp/PerformanceTest.Server/Properties/launchSettings.json
@@ -6,7 +6,8 @@
       "launchBrowser": false,
       "applicationUrl": "http://localhost:5000;https://localhost:5001",
       "environmentVariables": {
-        "ASPNETCORE_ENVIRONMENT": "Development"
+        "ASPNETCORE_ENVIRONMENT": "Development",
+        "Kestrel__Endpoints__Grpc__Url": "http://localhost:5000"
       }
     }
   }
diff --git a/perf/BenchmarkApp/PerformanceTest.Server/appsettings.json b/perf/BenchmarkApp/PerformanceTest.Server/appsettings.json
index 1aef5074f..02b5fa21c 100644
--- a/perf/BenchmarkApp/PerformanceTest.Server/appsettings.json
+++ b/perf/BenchmarkApp/PerformanceTest.Server/appsettings.json
@@ -9,6 +9,12 @@
   "Kestrel": {
     "EndpointDefaults": {
       "Protocols": "Http2"
+    },
+    "Endpoints": {
+      "Grpc": {
+        "Url": "http://+:5000",
+        "Protocols": "Http2"
+      }
     }
   }
 }