Repository: kedacore/http-add-on
Pull request #961 (2024-04-01 20:37:24)

feat: Support aggregation options
Until this PR, we only supported scaling based on "pending" requests. This can work on high load scenarios as it measures directly the amount of requests on a given moment, but it doesn't work for other scenarios because the scaler doesn't aggregate the requests in any way. 

With this PR, the add-on will support 2 scaling options, based on concurrency (current approach just renamed) and based on the request rate for a given time window (more reasons for the renaming below). 

These options are configured though a new section:

```yaml
spec
  scalingMetric: # requestRate and concurrency are mutually exclusive
    requestRate:
      granularity: 1s
      targetValue: 100
      window: 1m
    concurrency:
      targetValue: 100
```

The idea behind this change is adding support (in the future) for both metrics as RPS is a good metric for regular scaling as it's more fuzzy thanks to the time window, but in the other hand, peaks are better handled by concurrency. Having this future change in mind, it makes sense to move the metric configuration to a nested section instead of sharing the `targetPendingRequests` key (which is also not aligned with new naming).

During the PR I've found some small changes (such as release process to include the docs update or the logger unification to have a better observability) that I've included as part of this PR to not forget them (or because they help me directly, like the logger change)

### Why have I renamed the scaling?
Although pending seems worth, being accurate we are scaling based on concurrent request (or in-flight), pending can sound like request not proxied yet, but already proxied requests are taken into account even though it's the backend who hasn't answered yet. Using concurrent, we are more accurate with the real scaling behavior. 
There is another reason behind this change and it's to be aligned with current [Knative scaling](https://knative.dev/docs/serving/autoscaling/autoscaling-metrics/#setting-metrics-per-revision) naming. At this moment, Knative is "the king" from HTTP scaling pov, so aligning our naming with them can make sense for making the things easier for end-users.

### Checklist

- [x] Commits are signed with Developer Certificate of Origin (DCO)
- [x] Changelog has been updated and is aligned with our [changelog requirements](https://github.com/kedacore/keda/blob/main/CONTRIBUTING.md#Changelog)
- [x] Any necessary documentation is added, such as:
  - [`README.md`](/README.md)
  - [The `docs/` directory](./docs)
  - [The docs repo](https://github.com/kedacore/keda-docs)

Fixes https://github.com/kedacore/http-add-on/issues/882
Fixes https://github.com/kedacore/http-add-on/issues/958



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b56e3e1..5d1ad59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,9 +24,12 @@ This changelog keeps track of work items that have been completed and are ready
 
 - **General**: Propagate HTTPScaledObject labels and annotations to ScaledObject ([#840](https://github.com/kedacore/http-add-on/issues/840))
 - **General**: Provide support to allow HTTP scaler to work alongside other core KEDA scalers ([#489](https://github.com/kedacore/http-add-on/issues/489))
+- **General**: Support aggregation windows ([#882](https://github.com/kedacore/http-add-on/issues/882))
 
 ### Improvements
 
+- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO))
+
 ### Fixes
 
 - **General**: Ensure operator is aware about changes on underlying ScaledObject ([#900](https://github.com/kedacore/http-add-on/issues/900))
@@ -35,16 +38,13 @@ This changelog keeps track of work items that have been completed and are ready
 
 You can find all deprecations in [this overview](https://github.com/kedacore/http-add-on/labels/breaking-change) and [join the discussion here](https://github.com/kedacore/http-add-on/discussions/categories/deprecations).
 
-New deprecation(s):
-
-
-Previously announced deprecation(s):
-
+- **General**: Deprecated `.spec.scaleTargetRef.deployment` ([#960](https://github.com/kedacore/http-add-on/discussions/960))
 - **General**: Deprecated `KEDA_HTTP_DEPLOYMENT_CACHE_POLLING_INTERVAL_MS` in favor of `KEDA_HTTP_ENDPOINTS_CACHE_POLLING_INTERVAL_MS` ([#438](https://github.com/kedacore/http-add-on/issues/438))
+- **General**: Deprecated `targetPendingRequests` in favor of `spec.scalingMetric.*.targetValue` ([#959](https://github.com/kedacore/http-add-on/discussions/959))
 
 ### Other
 
-- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO))
+- **General**: Unify loggers ([#958](https://github.com/kedacore/http-add-on/issues/958))
 
 ## v0.7.0
 
diff --git a/RELEASE-PROCESS.md b/RELEASE-PROCESS.md
index fcb95fd..6dac819 100644
--- a/RELEASE-PROCESS.md
+++ b/RELEASE-PROCESS.md
@@ -20,7 +20,17 @@ It should not include every single change but solely what matters to our custome
 
 Add the new released version to the list in `KEDA Version` dropdown in [2_bug_report.yml](https://github.com/kedacore/http-add-on/blob/main/.github/ISSUE_TEMPLATE/2_bug_report.yml).
 
-## 4: Create a new GitHub release
+## 4. Update documentation references to current version
+
+Update the links to current version within the file `walkthrough.md`
+
+> ```console
+> kubectl create -n $NAMESPACE -f examples/v0.7.0/httpscaledobject.yaml
+> ```
+
+> >If you'd like to learn more about this object, please see the [`HTTPScaledObject` reference](THE REFERENCE).
+
+## 5: Create a new GitHub release
 
 [Create a new release](https://github.com/kedacore/http-add-on/releases/new) on the GitHub releases page, using your new release number.
 
@@ -30,7 +40,7 @@ The release description should be a short to medium length summary of what has c
 
 After you create the new release, automation in a GitHub action will build and deploy new container images.
 
-## 5: Submit a PR to the [Helm Charts Repository](https://github.com/kedacore/charts)
+## 6: Submit a PR to the [Helm Charts Repository](https://github.com/kedacore/charts)
 
 The scope of the changes you'll need to make to the Helm chart vary, but the below list is the minimum set of fields to change:
 
@@ -66,11 +76,11 @@ Once you've made changes to the chart, here's how to do submit the change to the
 
 After your PR is merged, you've completed the release. Congratulations! You can optionally write a blog post about it; see the next section if you're interested.
 
-## 6: Publish release on Artifact Hub
+## 7: Publish release on Artifact Hub
 
 Publish release on Artifact Hub by creating a new version in [kedacore/external-scalers](https://github.com/kedacore/external-scalers/tree/main/artifacthub/add-ons-http).
 
-## 7: Write a blog post on the documentation site (_optional_)
+## 8: Write a blog post on the documentation site (_optional_)
 
 If you believe that your release is large enough to warrant a blog post on the [keda.sh/blog](https://keda.sh/blog/) site, please go to [github.com/kedacore/keda-docs](https://github.com/kedacore/keda-docs) and submit a new PR with a blog article about the release.
 
diff --git a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml
index d2d29ab..cceade2 100644
--- a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml
+++ b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml
@@ -121,8 +121,39 @@ spec:
                 description: (optional) Cooldown period value
                 format: int32
                 type: integer
+              scalingMetric:
+                description: (optional) Configuration for the metric used for scaling
+                properties:
+                  concurrency:
+                    description: Scaling based on concurrent requests for a given
+                      target
+                    properties:
+                      targetValue:
+                        default: 100
+                        description: Target value for rate scaling
+                        type: integer
+                    type: object
+                  requestRate:
+                    description: Scaling based the average rate during an specific
+                      time window for a given target
+                    properties:
+                      granularity:
+                        default: 1s
+                        description: Time granularity for rate calculation
+                        type: string
+                      targetValue:
+                        default: 100
+                        description: Target value for rate scaling
+                        type: integer
+                      window:
+                        default: 1m
+                        description: Time window for rate calculation
+                        type: string
+                    type: object
+                type: object
               targetPendingRequests:
-                description: (optional) Target metric value
+                description: (optional) DEPRECATED (use SscalingMetric instead) Target
+                  metric value
                 format: int32
                 type: integer
             required:
diff --git a/docs/ref/v0.6.0/http_scaled_object.md b/docs/ref/v0.6.0/http_scaled_object.md
index d541498..eba4ce6 100644
--- a/docs/ref/v0.6.0/http_scaled_object.md
+++ b/docs/ref/v0.6.0/http_scaled_object.md
@@ -1,6 +1,6 @@
 # The `HTTPScaledObject`
 
->This document reflects the specification of the `HTTPScaledObject` resource for the `v0.7.0` version.
+>This document reflects the specification of the `HTTPScaledObject` resource for the `v0.6.0` version.
 
 Each `HTTPScaledObject` looks approximately like the below:
 
diff --git a/docs/ref/v0.8.0/http_scaled_object.md b/docs/ref/v0.8.0/http_scaled_object.md
new file mode 100644
index 0000000..3a03515
--- /dev/null
+++ b/docs/ref/v0.8.0/http_scaled_object.md
@@ -0,0 +1,137 @@
+# The `HTTPScaledObject`
+
+>This document reflects the specification of the `HTTPScaledObject` resource for the `v0.8.0` version.
+
+Each `HTTPScaledObject` looks approximately like the below:
+
+```yaml
+kind: HTTPScaledObject
+apiVersion: http.keda.sh/v1alpha1
+metadata:
+    name: xkcd
+spec:
+    hosts:
+    - myhost.com
+    pathPrefixes:
+    - /test
+    scaleTargetRef:
+        name: xkcd
+        kind: Deployment
+        apiVersion: apps/v1
+        service: xkcd
+        port: 8080
+    replicas:
+        min: 5
+        max: 10
+    scaledownPeriod: 300
+    scalingMetric: # requestRate and concurrency are mutually exclusive
+        requestRate:
+            granularity: 1s
+            targetValue: 100
+            window: 1m
+        concurrency:
+            targetValue: 100
+```
+
+This document is a narrated reference guide for the `HTTPScaledObject`, and we'll focus on the `spec` field.
+
+## `hosts`
+
+These are the hosts to apply this scaling rule to. All incoming requests with one of these values in their `Host` header will be forwarded to the `Service` and port specified in the below `scaleTargetRef`, and that same `scaleTargetRef`'s workload will be scaled accordingly.
+
+## `pathPrefixes`
+
+>Default: "/"
+
+These are the paths to apply this scaling rule to. All incoming requests with one of these values as path prefix will be forwarded to the `Service` and port specified in the below `scaleTargetRef`, and that same `scaleTargetRef`'s workload will be scaled accordingly.
+
+## `scaleTargetRef`
+
+This is the primary and most important part of the `spec` because it describes:
+
+1. The incoming host to apply this scaling rule to.
+2. What workload to scale.
+3. The service to which to route HTTP traffic.
+
+### `deployment` (DEPRECTATED: removed as part of v0.9.0)
+
+This is the name of the `Deployment` to scale. It must exist in the same namespace as this `HTTPScaledObject` and shouldn't be managed by any other autoscaling system. This means that there should not be any `ScaledObject` already created for this `Deployment`. The HTTP Add-on will manage a `ScaledObject` internally.
+
+### `name`
+
+This is the name of the workload to scale. It must exist in the same namespace as this `HTTPScaledObject` and shouldn't be managed by any other autoscaling system. This means that there should not be any `ScaledObject` already created for this workload. The HTTP Add-on will manage a `ScaledObject` internally.
+
+### `kind`
+
+This is the kind of the workload to scale.
+
+### `apiVersion`
+
+This is the apiVersion of the workload to scale.
+
+### `service`
+
+This is the name of the service to route traffic to. The add-on will create autoscaling and routing components that route to this `Service`. It must exist in the same namespace as this `HTTPScaledObject` and should route to the same `Deployment` as you entered in the `deployment` field.
+
+### `port`
+
+This is the port to route to on the service that you specified in the `service` field. It should be exposed on the service and should route to a valid `containerPort` on the `Deployment` you gave in the `deployment` field.
+
+### `targetPendingRequests` (DEPRECTATED: removed as part of v0.9.0)
+
+>Default: 100
+
+This is the number of _pending_ (or in-progress) requests that your application needs to have before the HTTP Add-on will scale it. Conversely, if your application has below this number of pending requests, the HTTP add-on will scale it down.
+
+For example, if you set this field to 100, the HTTP Add-on will scale your app up if it sees that there are 200 in-progress requests. On the other hand, it will scale down if it sees that there are only 20 in-progress requests. Note that it will _never_ scale your app to zero replicas unless there are _no_ requests in-progress. Even if you set this value to a very high number and only have a single in-progress request, your app will still have one replica.
+
+### `scaledownPeriod`
+
+>Default: 300
+
+The period to wait after the last reported active before scaling the resource back to 0.
+
+> Note: This time is measured on KEDA side based on in-flight requests, so workloads with few and random traffic could have unexpected scale to 0 cases. In those case we recommend to extend this period to ensure it doesn't happen.
+
+
+## `scalingMetric`
+
+This is the second most important part of the `spec` because it describes how the workload has to scale. This section contains 2 nested sections (`requestRate` and `concurrency`) which are mutually exclusive between themselves.
+
+### `requestRate`
+
+This section enables scaling based on the request rate.
+
+> **NOTE**: Requests information is stored in memory, aggragating long periods (longer than 5 minutes) or too fine granularity (less than 1 second) could produce perfomance issues or memory usage increase.
+
+> **NOTE 2**: Although updating `window` and/or `granularity` is something doable, the process just replaces all the stored request count infomation. This can produce unexpected scaling behaviours until the window is populated again.
+
+#### `targetValue`
+
+>Default: 100
+
+This is the target value for the scaling configuration.
+
+#### `window`
+
+>Default: "1m"
+
+This value defines the aggregation window for the request rate calculation.
+
+#### `granularity`
+
+>Default: "1s"
+
+This value defines the granualarity of the aggregated requests for the request rate calculation.
+
+### `concurrency`
+
+This section enables scaling based on the request concurrency.
+
+> **NOTE**: This is the only scaling behaviour before v0.8.0
+
+#### `targetValue`
+
+>Default: 100
+
+This is the target value for the scaling configuration.
diff --git a/docs/walkthrough.md b/docs/walkthrough.md
index 3cae7ee..4d4668a 100644
--- a/docs/walkthrough.md
+++ b/docs/walkthrough.md
@@ -95,7 +95,7 @@ For scenerios where you want to integrate HTTP Add-On scaler with other keda sca
 
 > 💡 Ensure that your ScaledObject is created with a different name than the `HTTPScaledObject` to ensure your ScaledObject is not removed by the reconciler.
 
-It is reccomended that you first deploy your HTTPScaledObject with no annotation set in order to obtain the latest trigger spec to use on your own managed ScaledObject.
+If you don't know how to set the external scaler in the ScaledObject, you can deploy first your HTTPScaledObject with no annotation set in order to obtain the latest trigger spec to use on your own managed ScaledObject.
 
 1. Deploy your `HTTPScaledObject` with annotation set to false
 
@@ -112,8 +112,7 @@ For example:
   triggers:
   - type: external-push
     metadata:
-      hosts: example-service
-      pathPrefixes: ""
+      httpScaledObject: YOUR_HTTPSCALEDOBJECT_NAME
       scalerAddress: keda-http-add-on-external-scaler.keda:9090
 ```
 
diff --git a/go.mod b/go.mod
index f46a9a0..7e9bf46 100644
--- a/go.mod
+++ b/go.mod
@@ -4,22 +4,21 @@ go 1.21
 
 require (
 	github.com/go-logr/logr v1.4.1
-	github.com/go-logr/zapr v1.3.0
 	github.com/golang/mock v1.7.0-rc.1.0.20220812172401-5b455625bd2c
+	github.com/google/go-cmp v0.6.0
 	github.com/hashicorp/go-immutable-radix/v2 v2.1.0
 	github.com/kedacore/keda/v2 v2.13.0
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/onsi/ginkgo/v2 v2.17.1
 	github.com/onsi/gomega v1.31.0
 	github.com/stretchr/testify v1.8.4
-	go.uber.org/zap v1.26.0
 	golang.org/x/sync v0.6.0
 	google.golang.org/grpc v1.61.0
 	google.golang.org/protobuf v1.33.0
 	k8s.io/api v0.29.0
 	k8s.io/apimachinery v0.29.0
 	k8s.io/client-go v0.29.0
-	k8s.io/code-generator v0.29.0
+	k8s.io/code-generator v0.29.3
 	k8s.io/utils v0.0.0-20240102154912-e7106e64919e
 	sigs.k8s.io/controller-runtime v0.17.1
 	sigs.k8s.io/kustomize/kustomize/v5 v5.3.0
@@ -35,15 +34,15 @@ require (
 	github.com/expr-lang/expr v1.15.8 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/go-errors/errors v1.5.1 // indirect
+	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.20.2 // indirect
 	github.com/go-openapi/jsonreference v0.20.4 // indirect
 	github.com/go-openapi/swag v0.22.7 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
-	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -72,6 +71,7 @@ require (
 	github.com/xlab/treeprint v1.2.0 // indirect
 	go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.26.0 // indirect
 	golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/net v0.20.0 // indirect
diff --git a/go.sum b/go.sum
index 06a5f29..730f95e 100644
--- a/go.sum
+++ b/go.sum
@@ -42,8 +42,8 @@ github.com/golang/mock v1.7.0-rc.1.0.20220812172401-5b455625bd2c h1:8AzxBXzXPCzl
 github.com/golang/mock v1.7.0-rc.1.0.20220812172401-5b455625bd2c/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
 github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -254,8 +254,8 @@ k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o=
 k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis=
 k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
 k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
-k8s.io/code-generator v0.29.0 h1:2LQfayGDhaIlaamXjIjEQlCMy4JNCH9lrzas4DNW1GQ=
-k8s.io/code-generator v0.29.0/go.mod h1:5bqIZoCxs2zTRKMWNYqyQWW/bajc+ah4rh0tMY8zdGA=
+k8s.io/code-generator v0.29.3 h1:m7E25/t9R9NvejspO2zBdyu+/Gl0Z5m7dCRc680KS14=
+k8s.io/code-generator v0.29.3/go.mod h1:x47ofBhN4gxYFcxeKA1PYXeaPreAGaDN85Y/lNUsPoM=
 k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s=
 k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M=
 k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks=
diff --git a/interceptor/main.go b/interceptor/main.go
index fd3734e..f2d2c14 100644
--- a/interceptor/main.go
+++ b/interceptor/main.go
@@ -3,6 +3,7 @@ package main
 import (
 	"context"
 	"errors"
+	"flag"
 	"fmt"
 	"net/http"
 	"os"
@@ -12,6 +13,7 @@ import (
 	"golang.org/x/sync/errgroup"
 	"k8s.io/client-go/kubernetes"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	"github.com/kedacore/http-add-on/interceptor/config"
 	"github.com/kedacore/http-add-on/interceptor/handler"
@@ -21,31 +23,37 @@ import (
 	"github.com/kedacore/http-add-on/pkg/build"
 	kedahttp "github.com/kedacore/http-add-on/pkg/http"
 	"github.com/kedacore/http-add-on/pkg/k8s"
-	pkglog "github.com/kedacore/http-add-on/pkg/log"
 	kedanet "github.com/kedacore/http-add-on/pkg/net"
 	"github.com/kedacore/http-add-on/pkg/queue"
 	"github.com/kedacore/http-add-on/pkg/routing"
 	"github.com/kedacore/http-add-on/pkg/util"
 )
 
+var (
+	setupLog = ctrl.Log.WithName("setup")
+)
+
 // +kubebuilder:rbac:groups=http.keda.sh,resources=httpscaledobjects,verbs=get;list;watch
 // +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch
 
 func main() {
-	lggr, err := pkglog.NewZapr()
-	if err != nil {
-		fmt.Println("Error building logger", err)
-		os.Exit(1)
-	}
-
 	timeoutCfg := config.MustParseTimeouts()
 	servingCfg := config.MustParseServing()
-	if err := config.Validate(servingCfg, *timeoutCfg, lggr); err != nil {
-		lggr.Error(err, "invalid configuration")
+
+	opts := zap.Options{
+		Development: true,
+	}
+	opts.BindFlags(flag.CommandLine)
+	flag.Parse()
+
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
+	if err := config.Validate(servingCfg, *timeoutCfg, ctrl.Log); err != nil {
+		setupLog.Error(err, "invalid configuration")
 		os.Exit(1)
 	}
 
-	lggr.Info(
+	setupLog.Info(
 		"starting interceptor",
 		"timeoutConfig",
 		timeoutCfg,
@@ -60,44 +68,45 @@ func main() {
 
 	cl, err := kubernetes.NewForConfig(cfg)
 	if err != nil {
-		lggr.Error(err, "creating new Kubernetes ClientSet")
+		setupLog.Error(err, "creating new Kubernetes ClientSet")
 		os.Exit(1)
 	}
 	endpointsCache := k8s.NewInformerBackedEndpointsCache(
-		lggr,
+		ctrl.Log,
 		cl,
 		time.Millisecond*time.Duration(servingCfg.EndpointsCachePollIntervalMS),
 	)
 	if err != nil {
-		lggr.Error(err, "creating new endpoints cache")
+		setupLog.Error(err, "creating new endpoints cache")
 		os.Exit(1)
 	}
-	waitFunc := newWorkloadReplicasForwardWaitFunc(lggr, endpointsCache)
+	waitFunc := newWorkloadReplicasForwardWaitFunc(ctrl.Log, endpointsCache)
 
 	httpCl, err := clientset.NewForConfig(cfg)
 	if err != nil {
-		lggr.Error(err, "creating new HTTP ClientSet")
+		setupLog.Error(err, "creating new HTTP ClientSet")
 		os.Exit(1)
 	}
+
+	queues := queue.NewMemory()
+
 	sharedInformerFactory := informers.NewSharedInformerFactory(httpCl, servingCfg.ConfigMapCacheRsyncPeriod)
-	routingTable, err := routing.NewTable(sharedInformerFactory, servingCfg.WatchNamespace)
+	routingTable, err := routing.NewTable(sharedInformerFactory, servingCfg.WatchNamespace, queues)
 	if err != nil {
-		lggr.Error(err, "fetching routing table")
+		setupLog.Error(err, "fetching routing table")
 		os.Exit(1)
 	}
 
-	q := queue.NewMemory()
-
-	lggr.Info("Interceptor starting")
+	setupLog.Info("Interceptor starting")
 
 	ctx := ctrl.SetupSignalHandler()
-	ctx = util.ContextWithLogger(ctx, lggr)
+	ctx = util.ContextWithLogger(ctx, ctrl.Log)
 
 	eg, ctx := errgroup.WithContext(ctx)
 
 	// start the endpoints cache updater
 	eg.Go(func() error {
-		lggr.Info("starting the endpoints cache")
+		setupLog.Info("starting the endpoints cache")
 
 		endpointsCache.Start(ctx)
 		return nil
@@ -107,10 +116,10 @@ func main() {
 	// the ConfigMap that the operator updates as HTTPScaledObjects
 	// enter and exit the system
 	eg.Go(func() error {
-		lggr.Info("starting the routing table")
+		setupLog.Info("starting the routing table")
 
 		if err := routingTable.Start(ctx); !util.IsIgnoredErr(err) {
-			lggr.Error(err, "routing table failed")
+			setupLog.Error(err, "routing table failed")
 			return err
 		}
 
@@ -120,10 +129,10 @@ func main() {
 	// start the administrative server. this is the server
 	// that serves the queue size API
 	eg.Go(func() error {
-		lggr.Info("starting the admin server", "port", adminPort)
+		setupLog.Info("starting the admin server", "port", adminPort)
 
-		if err := runAdminServer(ctx, lggr, q, adminPort); !util.IsIgnoredErr(err) {
-			lggr.Error(err, "admin server failed")
+		if err := runAdminServer(ctx, ctrl.Log, queues, adminPort); !util.IsIgnoredErr(err) {
+			setupLog.Error(err, "admin server failed")
 			return err
 		}
 
@@ -133,24 +142,24 @@ func main() {
 	// start the proxy server. this is the server that
 	// accepts, holds and forwards user requests
 	eg.Go(func() error {
-		lggr.Info("starting the proxy server", "port", proxyPort)
+		setupLog.Info("starting the proxy server", "port", proxyPort)
 
-		if err := runProxyServer(ctx, lggr, q, waitFunc, routingTable, timeoutCfg, proxyPort); !util.IsIgnoredErr(err) {
-			lggr.Error(err, "proxy server failed")
+		if err := runProxyServer(ctx, ctrl.Log, queues, waitFunc, routingTable, timeoutCfg, proxyPort); !util.IsIgnoredErr(err) {
+			setupLog.Error(err, "proxy server failed")
 			return err
 		}
 
 		return nil
 	})
 
-	build.PrintComponentInfo(lggr, "Interceptor")
+	build.PrintComponentInfo(ctrl.Log, "Interceptor")
 
 	if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {
-		lggr.Error(err, "fatal error")
+		setupLog.Error(err, "fatal error")
 		os.Exit(1)
 	}
 
-	lggr.Info("Bye!")
+	setupLog.Info("Bye!")
 }
 
 func runAdminServer(
diff --git a/interceptor/middleware/counting.go b/interceptor/middleware/counting.go
index 9529242..7036c5f 100644
--- a/interceptor/middleware/counting.go
+++ b/interceptor/middleware/counting.go
@@ -62,7 +62,7 @@ func (cm *Counting) count(ctx context.Context, signaler util.Signaler) {
 }
 
 func (cm *Counting) inc(logger logr.Logger, key string) bool {
-	if err := cm.queueCounter.Resize(key, +1); err != nil {
+	if err := cm.queueCounter.Increase(key, 1); err != nil {
 		logger.Error(err, "error incrementing queue counter", "key", key)
 
 		return false
@@ -72,7 +72,7 @@ func (cm *Counting) inc(logger logr.Logger, key string) bool {
 }
 
 func (cm *Counting) dec(logger logr.Logger, key string) bool {
-	if err := cm.queueCounter.Resize(key, -1); err != nil {
+	if err := cm.queueCounter.Decrease(key, 1); err != nil {
 		logger.Error(err, "error decrementing queue counter", "key", key)
 
 		return false
diff --git a/interceptor/middleware/routing.go b/interceptor/middleware/routing.go
index c92d803..41dd108 100644
--- a/interceptor/middleware/routing.go
+++ b/interceptor/middleware/routing.go
@@ -13,8 +13,8 @@ import (
 )
 
 var (
-	kpUserAgent = regexp.MustCompile(`(^|\s)kube-probe/`)
-	ghUserAgent = regexp.MustCompile(`(^|\s)GoogleHC/`)
+	kubernetesProbeUserAgent = regexp.MustCompile(`(^|\s)kube-probe/`)
+	googleHCUserAgent        = regexp.MustCompile(`(^|\s)GoogleHC/`)
 )
 
 type Routing struct {
@@ -75,5 +75,5 @@ func (rm *Routing) streamFromHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) (*url
 func (rm *Routing) isProbe(r *http.Request) bool {
 	ua := r.UserAgent()
 
-	return kpUserAgent.Match([]byte(ua)) || ghUserAgent.Match([]byte(ua))
+	return kubernetesProbeUserAgent.Match([]byte(ua)) || googleHCUserAgent.Match([]byte(ua))
 }
diff --git a/operator/apis/http/v1alpha1/httpscaledobject_types.go b/operator/apis/http/v1alpha1/httpscaledobject_types.go
index fd8a8ce..3187054 100644
--- a/operator/apis/http/v1alpha1/httpscaledobject_types.go
+++ b/operator/apis/http/v1alpha1/httpscaledobject_types.go
@@ -45,6 +45,38 @@ type ReplicaStruct struct {
 	Max *int32 `json:"max,omitempty" description:"Maximum amount of replicas to have in the deployment (Default 100)"`
 }
 
+// ScalingMetricSpec contains the scaling calculation type
+type ScalingMetricSpec struct {
+	// Scaling based on concurrent requests for a given target
+	Concurrency *ConcurrencyMetricSpec `json:"concurrency,omitempty" description:"Scaling based on concurrent requests for a given target. 'concurrency' and 'rate' are mutually exclusive."`
+	// Scaling based the average rate during an specific time window for a given target
+	Rate *RateMetricSpec `json:"requestRate,omitempty" description:"Scaling based the average rate during an specific time window for a given target. 'concurrency' and 'rate' are mutually exclusive."`
+}
+
+// ConcurrencyMetricSpec defines the concurrency scaling
+type ConcurrencyMetricSpec struct {
+	// Target value for rate scaling
+	// +kubebuilder:default=100
+	// +optional
+	TargetValue int `json:"targetValue" description:"Target value for concurrency scaling"`
+}
+
+// RateMetricSpec defines the concurrency scaling
+type RateMetricSpec struct {
+	// Target value for rate scaling
+	// +kubebuilder:default=100
+	// +optional
+	TargetValue int `json:"targetValue" description:"Target value for rate scaling"`
+	// Time window for rate calculation
+	// +kubebuilder:default="1m"
+	// +optional
+	Window metav1.Duration `json:"window" description:"Time window for rate calculation"`
+	// Time granularity for rate calculation
+	// +kubebuilder:default="1s"
+	// +optional
+	Granularity metav1.Duration `json:"granularity" description:"Time granularity for rate calculation"`
+}
+
 // HTTPScaledObjectSpec defines the desired state of HTTPScaledObject
 type HTTPScaledObjectSpec struct {
 	// The hosts to route. All requests which the "Host" header
@@ -63,12 +95,15 @@ type HTTPScaledObjectSpec struct {
 	// (optional) Replica information
 	// +optional
 	Replicas *ReplicaStruct `json:"replicas,omitempty"`
-	// (optional) Target metric value
+	// (optional) DEPRECATED (use SscalingMetric instead) Target metric value
 	// +optional
 	TargetPendingRequests *int32 `json:"targetPendingRequests,omitempty" description:"The target metric value for the HPA (Default 100)"`
 	// (optional) Cooldown period value
 	// +optional
 	CooldownPeriod *int32 `json:"scaledownPeriod,omitempty" description:"Cooldown period (seconds) for resources to scale down (Default 300)"`
+	// (optional) Configuration for the metric used for scaling
+	// +optional
+	ScalingMetric *ScalingMetricSpec `json:"scalingMetric,omitempty" description:"Configuration for the metric used for scaling. If empty 'concurrency' will be used"`
 }
 
 // HTTPScaledObjectStatus defines the observed state of HTTPScaledObject
diff --git a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go
index 7004146..27d7726 100644
--- a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go
+++ b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go
@@ -24,6 +24,21 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConcurrencyMetricSpec) DeepCopyInto(out *ConcurrencyMetricSpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConcurrencyMetricSpec.
+func (in *ConcurrencyMetricSpec) DeepCopy() *ConcurrencyMetricSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ConcurrencyMetricSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in Conditions) DeepCopyInto(out *Conditions) {
 	{
@@ -146,6 +161,11 @@ func (in *HTTPScaledObjectSpec) DeepCopyInto(out *HTTPScaledObjectSpec) {
 		*out = new(int32)
 		**out = **in
 	}
+	if in.ScalingMetric != nil {
+		in, out := &in.ScalingMetric, &out.ScalingMetric
+		*out = new(ScalingMetricSpec)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScaledObjectSpec.
@@ -178,6 +198,23 @@ func (in *HTTPScaledObjectStatus) DeepCopy() *HTTPScaledObjectStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RateMetricSpec) DeepCopyInto(out *RateMetricSpec) {
+	*out = *in
+	out.Window = in.Window
+	out.Granularity = in.Granularity
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateMetricSpec.
+func (in *RateMetricSpec) DeepCopy() *RateMetricSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(RateMetricSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ReplicaStruct) DeepCopyInto(out *ReplicaStruct) {
 	*out = *in
@@ -217,3 +254,28 @@ func (in *ScaleTargetRef) DeepCopy() *ScaleTargetRef {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ScalingMetricSpec) DeepCopyInto(out *ScalingMetricSpec) {
+	*out = *in
+	if in.Concurrency != nil {
+		in, out := &in.Concurrency, &out.Concurrency
+		*out = new(ConcurrencyMetricSpec)
+		**out = **in
+	}
+	if in.Rate != nil {
+		in, out := &in.Rate, &out.Rate
+		*out = new(RateMetricSpec)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalingMetricSpec.
+func (in *ScalingMetricSpec) DeepCopy() *ScalingMetricSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ScalingMetricSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/operator/controllers/http/httpscaledobject_controller.go b/operator/controllers/http/httpscaledobject_controller.go
index 721f075..6951d8b 100644
--- a/operator/controllers/http/httpscaledobject_controller.go
+++ b/operator/controllers/http/httpscaledobject_controller.go
@@ -110,6 +110,15 @@ func (r *HTTPScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Req
 		httpso.Name,
 	)
 
+	// check if ScalingMetric is correct
+	if httpso.Spec.ScalingMetric != nil &&
+		httpso.Spec.ScalingMetric.Concurrency != nil &&
+		httpso.Spec.ScalingMetric.Rate != nil {
+		err := fmt.Errorf(".spec.scalingMetric.concurrency and .spec.scalingMetric.rate are mutually exclusive")
+		logger.Error(err, "invalid values for .spec.scalingMetric")
+		return ctrl.Result{}, err
+	}
+
 	// Create required app objects for the application defined by the CRD
 	err := r.createOrUpdateApplicationResources(
 		ctx,
diff --git a/operator/controllers/http/scaled_object.go b/operator/controllers/http/scaled_object.go
index 832c8f5..d628065 100644
--- a/operator/controllers/http/scaled_object.go
+++ b/operator/controllers/http/scaled_object.go
@@ -41,8 +41,6 @@ func (r *HTTPScaledObjectReconciler) createOrUpdateScaledObject(
 		httpso.Annotations,
 		httpso.Spec.ScaleTargetRef,
 		externalScalerHostName,
-		httpso.Spec.Hosts,
-		httpso.Spec.PathPrefixes,
 		minReplicaCount,
 		maxReplicaCount,
 		httpso.Spec.CooldownPeriod,
diff --git a/pkg/k8s/scaledobject.go b/pkg/k8s/scaledobject.go
index 15e49f4..8b1075c 100644
--- a/pkg/k8s/scaledobject.go
+++ b/pkg/k8s/scaledobject.go
@@ -1,8 +1,6 @@
 package k8s
 
 import (
-	"strings"
-
 	kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
@@ -14,9 +12,8 @@ const (
 	soPollingInterval = 15
 	soTriggerType     = "external-push"
 
-	mkScalerAddress = "scalerAddress"
-	mkHosts         = "hosts"
-	mkPathPrefixes  = "pathPrefixes"
+	ScalerAddressKey    = "scalerAddress"
+	HTTPScaledObjectKey = "httpScaledObject"
 )
 
 // NewScaledObject creates a new ScaledObject in memory
@@ -27,8 +24,6 @@ func NewScaledObject(
 	annotations map[string]string,
 	workloadRef v1alpha1.ScaleTargetRef,
 	scalerAddress string,
-	hosts []string,
-	pathPrefixes []string,
 	minReplicas *int32,
 	maxReplicas *int32,
 	cooldownPeriod *int32,
@@ -61,9 +56,8 @@ func NewScaledObject(
 				{
 					Type: soTriggerType,
 					Metadata: map[string]string{
-						mkScalerAddress: scalerAddress,
-						mkHosts:         strings.Join(hosts, ","),
-						mkPathPrefixes:  strings.Join(pathPrefixes, ","),
+						ScalerAddressKey:    scalerAddress,
+						HTTPScaledObjectKey: name,
 					},
 				},
 			},
diff --git a/pkg/log/zapr.go b/pkg/log/zapr.go
deleted file mode 100644
index 8e393fc..0000000
--- a/pkg/log/zapr.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package log
-
-import (
-	"github.com/go-logr/logr"
-	"github.com/go-logr/zapr"
-	"go.uber.org/zap"
-)
-
-func NewZapr() (logr.Logger, error) {
-	zapCfg := zap.NewProductionConfig()
-	zapCfg.Sampling = &zap.SamplingConfig{
-		Initial:    1,
-		Thereafter: 5,
-	}
-	zapLggr, err := zapCfg.Build()
-	if err != nil {
-		return logr.Logger{}, err
-	}
-	return zapr.NewLogger(zapLggr), nil
-}
diff --git a/pkg/queue/bucketing.go b/pkg/queue/bucketing.go
new file mode 100644
index 0000000..b89b6eb
--- /dev/null
+++ b/pkg/queue/bucketing.go
@@ -0,0 +1,188 @@
+// Based on https://github.com/knative/serving/blob/main/pkg/autoscaler/aggregation/bucketing.go
+
+package queue
+
+import (
+	"math"
+	"sync"
+	"time"
+)
+
+const precision = 3
+
+// RequestsBuckets keeps buckets that have been collected at a certain time.
+type RequestsBuckets struct {
+	bucketsMutex sync.RWMutex
+	// buckets is a ring buffer indexed by timeToIndex() % len(buckets).
+	// Each element represents a certain granularity of time, and the total
+	// represented duration adds up to a window length of time.
+	buckets []int
+
+	// firstWrite holds the time when the first write has been made.
+	// This time is reset to `now` when the very first write happens,
+	// or when a first write happens after `window` time of inactivity.
+	// The difference between `now` and `firstWrite` is used to compute
+	// the number of eligible buckets for computation of average values.
+	firstWrite time.Time
+
+	// lastWrite stores the time when the last write was made.
+	// This is used to detect when we have gaps in the data (i.e. more than a
+	// granularity has expired since the last write) so that we can zero those
+	// entries in the buckets array. It is also used when calculating the
+	// WindowAverage to know how much of the buckets array represents valid data.
+	lastWrite time.Time
+
+	// granularity is the duration represented by each bucket in the buckets ring buffer.
+	granularity time.Duration
+	// window is the total time represented by the buckets ring buffer.
+	window time.Duration
+	// The total sum of all buckets within the window. This total includes
+	// invalid buckets, e.g. buckets written to before firstTime or after
+	// lastTime are included in this total.
+	windowTotal int
+}
+
+// NewRequestsBuckets generates a new RequestsBuckets with the given
+// granularity.
+func NewRequestsBuckets(window, granularity time.Duration) *RequestsBuckets {
+	// Number of buckets is `window` divided by `granularity`, rounded up.
+	// e.g. 60s / 2s = 30.
+	nb := math.Ceil(float64(window) / float64(granularity))
+	return &RequestsBuckets{
+		buckets:     make([]int, int(nb)),
+		granularity: granularity,
+		window:      window,
+	}
+}
+
+// IsEmpty returns true if no data has been recorded for the `window` period.
+func (t *RequestsBuckets) IsEmpty(now time.Time) bool {
+	now = now.Truncate(t.granularity)
+	t.bucketsMutex.RLock()
+	defer t.bucketsMutex.RUnlock()
+	return t.isEmptyLocked(now)
+}
+
+// isEmptyLocked expects `now` to be truncated and at least Read Lock held.
+func (t *RequestsBuckets) isEmptyLocked(now time.Time) bool {
+	return now.Sub(t.lastWrite) > t.window
+}
+
+func roundToNDigits(n int, f float64) float64 {
+	p := math.Pow10(n)
+	return math.Floor(f*p) / p
+}
+
+// WindowAverage returns the average bucket value over the window.
+//
+// If the first write was less than the window length ago, an average is
+// returned over the partial window. For example, if firstWrite was 6 seconds
+// ago, the average will be over these 6 seconds worth of buckets, even if the
+// window is 60s. If a window passes with no data being received, the first
+// write time is reset so this behaviour takes effect again.
+//
+// Similarly, if we have not received recent data, the average is based on a
+// partial window. For example, if the window is 60 seconds but we last
+// received data 10 seconds ago, the window average will be the average over
+// the first 50 seconds.
+//
+// In other cases, for example if there are gaps in the data shorter than the
+// window length, the missing data is assumed to be 0 and the average is over
+// the whole window length inclusive of the missing data.
+func (t *RequestsBuckets) WindowAverage(now time.Time) float64 {
+	now = now.Truncate(t.granularity)
+	t.bucketsMutex.RLock()
+	defer t.bucketsMutex.RUnlock()
+	switch d := now.Sub(t.lastWrite); {
+	case d <= 0:
+		// If LastWrite equal or greater than Now
+		// return the current WindowTotal, divided by the
+		// number of valid buckets
+		numB := math.Min(
+			float64(t.lastWrite.Sub(t.firstWrite)/t.granularity)+1, // +1 since the times are inclusive.
+			float64(len(t.buckets)))
+		return roundToNDigits(precision, float64(t.windowTotal)/numB)
+	case d < t.window:
+		// If we haven't received metrics for some time, which is less than
+		// the window -- remove the outdated items and divide by the number
+		// of valid buckets
+		stIdx := t.timeToIndex(t.lastWrite)
+		eIdx := t.timeToIndex(now)
+		ret := t.windowTotal
+		for i := stIdx + 1; i <= eIdx; i++ {
+			ret -= t.buckets[i%len(t.buckets)]
+		}
+		numB := math.Min(
+			float64(t.lastWrite.Sub(t.firstWrite)/t.granularity)+1, // +1 since the times are inclusive.
+			float64(len(t.buckets)-(eIdx-stIdx)))
+		return roundToNDigits(precision, float64(ret)/numB)
+	default: // Nothing for more than a window time, just 0.
+		return 0.
+	}
+}
+
+// timeToIndex converts time to an integer that can be used for modulo
+// operations to find the index in the bucket list.
+// bucketMutex needs to be held.
+func (t *RequestsBuckets) timeToIndex(tm time.Time) int {
+	// I don't think this run in 2038 :-)
+	// NB: we need to divide by granularity, since it's a compressing mapping
+	// to buckets.
+	return int(tm.Unix()) / int(t.granularity.Seconds())
+}
+
+// Record adds a value with an associated time to the correct bucket.
+// If this record would introduce a gap in the data, any intervening times
+// between the last write and this one will be recorded as zero. If an entire
+// window length has expired without data, the firstWrite time is reset,
+// meaning the WindowAverage will be of a partial window until enough data is
+// received to fill it again.
+func (t *RequestsBuckets) Record(now time.Time, value int) {
+	bucketTime := now.Truncate(t.granularity)
+
+	t.bucketsMutex.Lock()
+	defer t.bucketsMutex.Unlock()
+
+	writeIdx := t.timeToIndex(now)
+
+	if t.lastWrite != bucketTime {
+		if bucketTime.Add(t.window).After(t.lastWrite) {
+			// If it is the first write or it happened before the first write which we
+			// have in record, update the firstWrite.
+			if t.firstWrite.IsZero() || t.firstWrite.After(bucketTime) {
+				t.firstWrite = bucketTime
+			}
+
+			if bucketTime.After(t.lastWrite) {
+				if bucketTime.Sub(t.lastWrite) >= t.window {
+					// This means we had no writes for the duration of `window`. So reset the firstWrite time.
+					t.firstWrite = bucketTime
+					// Reset all the buckets.
+					for i := range t.buckets {
+						t.buckets[i] = 0
+					}
+					t.windowTotal = 0
+				} else {
+					// In theory we might lose buckets between stats gathering.
+					// Thus we need to clean not only the current index, but also
+					// all the ones from the last write. This is slower than the loop above
+					// due to possible wrap-around, so they are not merged together.
+					for i := t.timeToIndex(t.lastWrite) + 1; i <= writeIdx; i++ {
+						idx := i % len(t.buckets)
+						t.windowTotal -= t.buckets[idx]
+						t.buckets[idx] = 0
+					}
+				}
+				// Update the last write time.
+				t.lastWrite = bucketTime
+			}
+			// The else case is t.lastWrite - t.window < bucketTime < t.lastWrite, we can simply add
+			// the value to the bucket.
+		} else {
+			// Ignore this value because it happened a window size ago.
+			return
+		}
+	}
+	t.buckets[writeIdx%len(t.buckets)] += value
+	t.windowTotal += value
+}
diff --git a/pkg/queue/queue.go b/pkg/queue/queue.go
index 36d9ccd..925c5ce 100644
--- a/pkg/queue/queue.go
+++ b/pkg/queue/queue.go
@@ -1,7 +1,9 @@
 package queue
 
 import (
+	"fmt"
 	"sync"
+	"time"
 )
 
 // CountReader represents the size of a virtual HTTP queue, possibly
@@ -24,60 +26,93 @@ type CountReader interface {
 // the read functionality is point-in-time only
 type Counter interface {
 	CountReader
-	// Resize resizes the queue size by delta for the given host.
-	Resize(host string, delta int) error
-	// Ensure ensures that host is represented in this counter.
-	// If host already has a nonzero value, then it is unchanged. If
-	// it is missing, it is set to 0.
-	Ensure(host string)
-	// Remove tries to remove the given host and its
+	// Increase increases the queue size by delta for the given host.
+	Increase(host string, delta int) error
+	// Decrease decreases the queue size by delta for the given host.
+	Decrease(host string, delta int) error
+	// EnsureKey ensures that host is represented in this counter.
+	EnsureKey(host string, window, granularity time.Duration)
+	// UpdateBuckets update request backets if there are changes
+	UpdateBuckets(host string, window, granularity time.Duration)
+	// RemoveKey tries to remove the given host and its
 	// associated counts from the queue. returns true if it existed,
 	// false otherwise.
-	Remove(host string) bool
+	RemoveKey(host string) bool
 }
 
+// Memory implements Counter and CountReader
+var _ Counter = (*Memory)(nil)
+var _ CountReader = (*Memory)(nil)
+
 // Memory is a Counter implementation that
 // holds the HTTP queue in memory only. Always use
 // NewMemory to create one of these.
 type Memory struct {
-	countMap map[string]int
-	mut      *sync.RWMutex
+	concurrentMap map[string]int
+	rpsMap        map[string]*RequestsBuckets
+	mut           *sync.RWMutex
 }
 
 // NewMemoryQueue creates a new empty in-memory queue
 func NewMemory() *Memory {
 	lock := new(sync.RWMutex)
 	return &Memory{
-		countMap: make(map[string]int),
-		mut:      lock,
+		concurrentMap: make(map[string]int),
+		rpsMap:        make(map[string]*RequestsBuckets),
+		mut:           lock,
 	}
 }
 
-// Resize changes the size of the queue. Further calls to Current() return
-// the newly calculated size if no other Resize() calls were made in the
-// interim.
-func (r *Memory) Resize(host string, delta int) error {
+// Increase changes the size of the queue adding delta
+func (r *Memory) Increase(host string, delta int) error {
 	r.mut.Lock()
 	defer r.mut.Unlock()
-	r.countMap[host] += delta
+	r.concurrentMap[host] += delta
+	r.rpsMap[host].Record(time.Now(), delta)
 	return nil
 }
 
-func (r *Memory) Ensure(host string) {
+// Decrease changes the size of the queue reducing delta
+func (r *Memory) Decrease(host string, delta int) error {
 	r.mut.Lock()
 	defer r.mut.Unlock()
-	_, ok := r.countMap[host]
+	r.concurrentMap[host] -= delta
+	return nil
+}
+
+func (r *Memory) EnsureKey(host string, window, granularity time.Duration) {
+	r.mut.Lock()
+	defer r.mut.Unlock()
+	_, ok := r.concurrentMap[host]
 	if !ok {
-		r.countMap[host] = 0
+		r.concurrentMap[host] = 0
+	}
+	_, ok = r.rpsMap[host]
+	if !ok {
+		r.rpsMap[host] = NewRequestsBuckets(window, granularity)
+	}
+}
+
+func (r *Memory) UpdateBuckets(host string, window, granularity time.Duration) {
+	r.EnsureKey(host, window, granularity)
+	r.mut.Lock()
+	defer r.mut.Unlock()
+	buckets, ok := r.rpsMap[host]
+	if ok &&
+		(buckets.window != window ||
+			buckets.granularity != granularity) {
+		r.rpsMap[host] = NewRequestsBuckets(window, granularity)
 	}
 }
 
-func (r *Memory) Remove(host string) bool {
+func (r *Memory) RemoveKey(host string) bool {
 	r.mut.Lock()
 	defer r.mut.Unlock()
-	_, ok := r.countMap[host]
-	delete(r.countMap, host)
-	return ok
+	_, concurrentOk := r.concurrentMap[host]
+	delete(r.concurrentMap, host)
+	_, rpsOk := r.rpsMap[host]
+	delete(r.rpsMap, host)
+	return concurrentOk && rpsOk
 }
 
 // Current returns the current size of the queue.
@@ -85,8 +120,15 @@ func (r *Memory) Current() (*Counts, error) {
 	r.mut.RLock()
 	defer r.mut.RUnlock()
 	cts := NewCounts()
-	for key, val := range r.countMap {
-		cts.Counts[key] = val
+	for key, concurrency := range r.concurrentMap {
+		rpsItem, ok := r.rpsMap[key]
+		if !ok {
+			return nil, fmt.Errorf(fmt.Sprintf("rps map doesn't contain the key '%s'", key))
+		}
+		cts.Counts[key] = Count{
+			Concurrency: concurrency,
+			RPS:         rpsItem.WindowAverage(time.Now()),
+		}
 	}
 	return cts, nil
 }
diff --git a/pkg/queue/queue_counts.go b/pkg/queue/queue_counts.go
index fc8ce5b..c0abba8 100644
--- a/pkg/queue/queue_counts.go
+++ b/pkg/queue/queue_counts.go
@@ -5,8 +5,16 @@ import (
 	"fmt"
 )
 
-// Counts is a snapshot of the HTTP pending request queue counts
-// for each host.
+// Count is a snapshot of the HTTP pending request (Concurrency)
+// count and RPS
+
+type Count struct {
+	Concurrency int
+	RPS         float64
+}
+
+// Count is a snapshot of the HTTP pending request (Concurrency)
+// count and RPS for each host.
 // This is a json.Marshaler, json.Unmarshaler, and fmt.Stringer
 // implementation.
 //
@@ -15,23 +23,24 @@ type Counts struct {
 	json.Marshaler
 	json.Unmarshaler
 	fmt.Stringer
-	Counts map[string]int
+	Counts map[string]Count
 }
 
 // NewQueueCounts creates a new empty QueueCounts struct
 func NewCounts() *Counts {
 	return &Counts{
-		Counts: map[string]int{},
+		Counts: map[string]Count{},
 	}
 }
 
 // Aggregate returns the total count across all hosts
-func (q *Counts) Aggregate() int {
-	agg := 0
+func (q *Counts) Aggregate() Count {
+	res := Count{}
 	for _, count := range q.Counts {
-		agg += count
+		res.Concurrency += count.Concurrency
+		res.RPS += count.RPS
 	}
-	return agg
+	return res
 }
 
 // MarshalJSON implements json.Marshaler
diff --git a/pkg/queue/queue_fakes.go b/pkg/queue/queue_fakes.go
index 9f0c989..d85df95 100644
--- a/pkg/queue/queue_fakes.go
+++ b/pkg/queue/queue_fakes.go
@@ -6,7 +6,7 @@ import (
 	"time"
 )
 
-var _ Counter = &FakeCounter{}
+var _ Counter = (*FakeCounter)(nil)
 
 type HostAndCount struct {
 	Host  string
@@ -14,7 +14,7 @@ type HostAndCount struct {
 }
 type FakeCounter struct {
 	mapMut        *sync.RWMutex
-	RetMap        map[string]int
+	RetMap        map[string]Count
 	ResizedCh     chan HostAndCount
 	ResizeTimeout time.Duration
 }
@@ -22,34 +22,58 @@ type FakeCounter struct {
 func NewFakeCounter() *FakeCounter {
 	return &FakeCounter{
 		mapMut:        new(sync.RWMutex),
-		RetMap:        map[string]int{},
+		RetMap:        map[string]Count{},
 		ResizedCh:     make(chan HostAndCount),
 		ResizeTimeout: 1 * time.Second,
 	}
 }
 
-func (f *FakeCounter) Resize(host string, i int) error {
+func (f *FakeCounter) Increase(host string, i int) error {
 	f.mapMut.Lock()
-	f.RetMap[host] += i
+	count := f.RetMap[host]
+	count.Concurrency += i
+	count.RPS += float64(i)
+	f.RetMap[host] = count
 	f.mapMut.Unlock()
 	select {
 	case f.ResizedCh <- HostAndCount{Host: host, Count: i}:
 	case <-time.After(f.ResizeTimeout):
 		return fmt.Errorf(
-			"FakeCounter.Resize timeout after %s",
+			"FakeCounter.Increase timeout after %s",
 			f.ResizeTimeout,
 		)
 	}
 	return nil
 }
 
-func (f *FakeCounter) Ensure(host string) {
+func (f *FakeCounter) Decrease(host string, i int) error {
+	f.mapMut.Lock()
+	count := f.RetMap[host]
+	count.Concurrency -= i
+	f.RetMap[host] = count
+	f.mapMut.Unlock()
+	select {
+	case f.ResizedCh <- HostAndCount{Host: host, Count: i}:
+	case <-time.After(f.ResizeTimeout):
+		return fmt.Errorf(
+			"FakeCounter.Decrease timeout after %s",
+			f.ResizeTimeout,
+		)
+	}
+	return nil
+}
+
+func (f *FakeCounter) EnsureKey(host string, _, _ time.Duration) {
 	f.mapMut.Lock()
 	defer f.mapMut.Unlock()
-	f.RetMap[host] = 0
+	f.RetMap[host] = Count{
+		Concurrency: 0,
+	}
 }
 
-func (f *FakeCounter) Remove(host string) bool {
+func (f *FakeCounter) UpdateBuckets(_ string, _, _ time.Duration) {}
+
+func (f *FakeCounter) RemoveKey(host string) bool {
 	f.mapMut.Lock()
 	defer f.mapMut.Unlock()
 	_, ok := f.RetMap[host]
@@ -69,14 +93,18 @@ func (f *FakeCounter) Current() (*Counts, error) {
 var _ CountReader = &FakeCountReader{}
 
 type FakeCountReader struct {
-	current int
-	err     error
+	concurrency int
+	rps         float64
+	err         error
 }
 
 func (f *FakeCountReader) Current() (*Counts, error) {
 	ret := NewCounts()
-	ret.Counts = map[string]int{
-		"sample.com": f.current,
+	ret.Counts = map[string]Count{
+		"sample.com": {
+			Concurrency: f.concurrency,
+			RPS:         f.rps,
+		},
 	}
 	return ret, f.err
 }
diff --git a/pkg/routing/table.go b/pkg/routing/table.go
index 2762d5d..4cf8a53 100644
--- a/pkg/routing/table.go
+++ b/pkg/routing/table.go
@@ -15,6 +15,7 @@ import (
 	"github.com/kedacore/http-add-on/operator/generated/informers/externalversions"
 	informershttpv1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1"
 	"github.com/kedacore/http-add-on/pkg/k8s"
+	"github.com/kedacore/http-add-on/pkg/queue"
 	"github.com/kedacore/http-add-on/pkg/util"
 )
 
@@ -40,9 +41,10 @@ type table struct {
 	httpScaledObjectsMutex                   sync.RWMutex
 	memoryHolder                             util.AtomicValue[TableMemory]
 	memorySignaler                           util.Signaler
+	queueCounter                             queue.Counter
 }
 
-func NewTable(sharedInformerFactory externalversions.SharedInformerFactory, namespace string) (Table, error) {
+func NewTable(sharedInformerFactory externalversions.SharedInformerFactory, namespace string, counter queue.Counter) (Table, error) {
 	httpScaledObjects := informershttpv1alpha1.New(sharedInformerFactory, namespace, nil).HTTPScaledObjects()
 
 	t := table{
@@ -61,7 +63,7 @@ func NewTable(sharedInformerFactory externalversions.SharedInformerFactory, name
 		return nil, err
 	}
 	t.httpScaledObjectEventHandlerRegistration = registration
-
+	t.queueCounter = counter
 	return &t, nil
 }
 
@@ -150,6 +152,15 @@ func (t *table) OnAdd(obj interface{}, _ bool) {
 	}
 	key := *k8s.NamespacedNameFromObject(httpScaledObject)
 
+	window := time.Minute
+	granualrity := time.Second
+	if httpScaledObject.Spec.ScalingMetric != nil &&
+		httpScaledObject.Spec.ScalingMetric.Rate != nil {
+		window = httpScaledObject.Spec.ScalingMetric.Rate.Window.Duration
+		granualrity = httpScaledObject.Spec.ScalingMetric.Rate.Granularity.Duration
+	}
+	t.queueCounter.EnsureKey(key.String(), window, granualrity)
+
 	defer t.memorySignaler.Signal()
 
 	t.httpScaledObjectsMutex.Lock()
@@ -171,8 +182,16 @@ func (t *table) OnUpdate(oldObj interface{}, newObj interface{}) {
 	}
 	newKey := *k8s.NamespacedNameFromObject(newHTTPSO)
 
-	mustDelete := oldKey != newKey
+	window := time.Minute
+	granualrity := time.Second
+	if newHTTPSO.Spec.ScalingMetric != nil &&
+		newHTTPSO.Spec.ScalingMetric.Rate != nil {
+		window = newHTTPSO.Spec.ScalingMetric.Rate.Window.Duration
+		granualrity = newHTTPSO.Spec.ScalingMetric.Rate.Granularity.Duration
+	}
+	t.queueCounter.UpdateBuckets(newKey.String(), window, granualrity)
 
+	mustDelete := oldKey != newKey
 	defer t.memorySignaler.Signal()
 
 	t.httpScaledObjectsMutex.Lock()
@@ -182,6 +201,7 @@ func (t *table) OnUpdate(oldObj interface{}, newObj interface{}) {
 
 	if mustDelete {
 		delete(t.httpScaledObjects, oldKey)
+		t.queueCounter.RemoveKey(oldKey.String())
 	}
 }
 
@@ -198,6 +218,8 @@ func (t *table) OnDelete(obj interface{}) {
 	defer t.httpScaledObjectsMutex.Unlock()
 
 	delete(t.httpScaledObjects, key)
+
+	t.queueCounter.RemoveKey(key.String())
 }
 
 var _ util.HealthChecker = (*table)(nil)
diff --git a/scaler/handlers.go b/scaler/handlers.go
index a572f06..0f7af97 100644
--- a/scaler/handlers.go
+++ b/scaler/handlers.go
@@ -7,6 +7,8 @@ package main
 import (
 	"context"
 	"errors"
+	"fmt"
+	"math"
 	"strconv"
 	"time"
 
@@ -145,13 +147,22 @@ func (e *impl) GetMetricSpec(
 		lggr.Error(err, "unable to get HTTPScaledObject", "name", sor.Name, "namespace", sor.Namespace)
 		return nil, err
 	}
-	targetPendingRequests := int64(ptr.Deref(httpso.Spec.TargetPendingRequests, 100))
+	targetValue := int64(ptr.Deref(httpso.Spec.TargetPendingRequests, 100))
+
+	if httpso.Spec.ScalingMetric != nil {
+		if httpso.Spec.ScalingMetric.Concurrency != nil {
+			targetValue = int64(httpso.Spec.ScalingMetric.Concurrency.TargetValue)
+		}
+		if httpso.Spec.ScalingMetric.Rate != nil {
+			targetValue = int64(httpso.Spec.ScalingMetric.Rate.TargetValue)
+		}
+	}
 
 	res := &externalscaler.GetMetricSpecResponse{
 		MetricSpecs: []*externalscaler.MetricSpec{
 			{
 				MetricName: metricName,
-				TargetSize: targetPendingRequests,
+				TargetSize: targetValue,
 			},
 		},
 	}
@@ -182,27 +193,49 @@ func (e *impl) GetMetrics(
 	_ context.Context,
 	metricRequest *externalscaler.GetMetricsRequest,
 ) (*externalscaler.GetMetricsResponse, error) {
+	lggr := e.lggr.WithName("GetMetrics")
 	sor := metricRequest.ScaledObjectRef
 
 	namespacedName := k8s.NamespacedNameFromScaledObjectRef(sor)
 	metricName := MetricName(namespacedName)
 
-	key := namespacedName.String()
-	count := int64(e.pinger.counts()[key])
-
-	if count == 0 {
+	scalerMetadata := sor.GetScalerMetadata()
+	httpScaledObjectName, ok := scalerMetadata[k8s.HTTPScaledObjectKey]
+	if !ok {
 		if scalerMetadata := sor.GetScalerMetadata(); scalerMetadata != nil {
 			if _, ok := scalerMetadata[keyInterceptorTargetPendingRequests]; ok {
 				return e.interceptorMetrics(metricName)
 			}
 		}
+		err := fmt.Errorf("unable to get HTTPScaledObject reference")
+		lggr.Error(err, "unable to get the linked HTTPScaledObject for ScaledObject", "name", sor.Name, "namespace", sor.Namespace)
+		return nil, err
+	}
+
+	httpso, err := e.httpsoInformer.Lister().HTTPScaledObjects(sor.Namespace).Get(httpScaledObjectName)
+	if err != nil {
+		lggr.Error(err, "unable to get HTTPScaledObject", "name", httpScaledObjectName, "namespace", sor.Namespace)
+		return nil, err
+	}
+
+	key := namespacedName.String()
+	count := e.pinger.counts()[key]
+
+	var metricValue int
+	if httpso.Spec.ScalingMetric != nil &&
+		httpso.Spec.ScalingMetric.Rate != nil {
+		metricValue = int(math.Ceil(count.RPS))
+		lggr.V(1).Info(fmt.Sprintf("%d rps for %s", metricValue, httpso.GetName()))
+	} else {
+		metricValue = count.Concurrency
+		lggr.V(1).Info(fmt.Sprintf("%d concurrent requests for %s", metricValue, httpso.GetName()))
 	}
 
 	res := &externalscaler.GetMetricsResponse{
 		MetricValues: []*externalscaler.MetricValue{
 			{
 				MetricName:  metricName,
-				MetricValue: count,
+				MetricValue: int64(metricValue),
 			},
 		},
 	}
@@ -214,7 +247,7 @@ func (e *impl) interceptorMetrics(metricName string) (*externalscaler.GetMetrics
 
 	var count int64
 	for _, v := range e.pinger.counts() {
-		count += int64(v)
+		count += int64(v.Concurrency)
 	}
 	if err := strconv.ErrRange; count < 0 {
 		lggr.Error(err, "count overflowed", "value", count)
diff --git a/scaler/main.go b/scaler/main.go
index 0691283..fd3d7ff 100644
--- a/scaler/main.go
+++ b/scaler/main.go
@@ -7,8 +7,8 @@ package main
 import (
 	"context"
 	"errors"
+	"flag"
 	"fmt"
-	"log"
 	"net"
 	"os"
 	"time"
@@ -22,25 +22,24 @@ import (
 	"google.golang.org/grpc/reflection"
 	"k8s.io/client-go/kubernetes"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	clientset "github.com/kedacore/http-add-on/operator/generated/clientset/versioned"
 	informers "github.com/kedacore/http-add-on/operator/generated/informers/externalversions"
 	informershttpv1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1"
 	"github.com/kedacore/http-add-on/pkg/build"
 	"github.com/kedacore/http-add-on/pkg/k8s"
-	pkglog "github.com/kedacore/http-add-on/pkg/log"
 	"github.com/kedacore/http-add-on/pkg/util"
 )
 
+var (
+	setupLog = ctrl.Log.WithName("setup")
+)
+
 // +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch
 // +kubebuilder:rbac:groups=http.keda.sh,resources=httpscaledobjects,verbs=get;list;watch
 
 func main() {
-	lggr, err := pkglog.NewZapr()
-	if err != nil {
-		log.Fatalf("error creating new logger (%v)", err)
-	}
-
 	cfg := mustParseConfig()
 	grpcPort := cfg.GRPCPort
 	namespace := cfg.TargetNamespace
@@ -49,18 +48,26 @@ func main() {
 	targetPortStr := fmt.Sprintf("%d", cfg.TargetPort)
 	targetPendingRequests := cfg.TargetPendingRequests
 
+	opts := zap.Options{
+		Development: true,
+	}
+	opts.BindFlags(flag.CommandLine)
+	flag.Parse()
+
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
 	k8sCfg, err := ctrl.GetConfig()
 	if err != nil {
-		lggr.Error(err, "Kubernetes client config not found")
+		setupLog.Error(err, "Kubernetes client config not found")
 		os.Exit(1)
 	}
 	k8sCl, err := kubernetes.NewForConfig(k8sCfg)
 	if err != nil {
-		lggr.Error(err, "creating new Kubernetes ClientSet")
+		setupLog.Error(err, "creating new Kubernetes ClientSet")
 		os.Exit(1)
 	}
 	pinger := newQueuePinger(
-		lggr,
+		ctrl.Log,
 		k8s.EndpointsFuncForK8sClientset(k8sCl),
 		namespace,
 		svcName,
@@ -70,27 +77,27 @@ func main() {
 
 	// create the endpoints informer
 	endpInformer := k8s.NewInformerBackedEndpointsCache(
-		lggr,
+		ctrl.Log,
 		k8sCl,
 		cfg.DeploymentCacheRsyncPeriod,
 	)
 
 	httpCl, err := clientset.NewForConfig(k8sCfg)
 	if err != nil {
-		lggr.Error(err, "creating new HTTP ClientSet")
+		setupLog.Error(err, "creating new HTTP ClientSet")
 		os.Exit(1)
 	}
 	sharedInformerFactory := informers.NewSharedInformerFactory(httpCl, cfg.ConfigMapCacheRsyncPeriod)
 	httpsoInformer := informershttpv1alpha1.New(sharedInformerFactory, "", nil).HTTPScaledObjects()
 
 	ctx := ctrl.SetupSignalHandler()
-	ctx = util.ContextWithLogger(ctx, lggr)
+	ctx = util.ContextWithLogger(ctx, setupLog)
 
 	eg, ctx := errgroup.WithContext(ctx)
 
 	// start the endpoints informer
 	eg.Go(func() error {
-		lggr.Info("starting the endpoints informer")
+		setupLog.Info("starting the endpoints informer")
 
 		endpInformer.Start(ctx)
 		return nil
@@ -98,17 +105,17 @@ func main() {
 
 	// start the httpso informer
 	eg.Go(func() error {
-		lggr.Info("starting the httpso informer")
+		setupLog.Info("starting the httpso informer")
 
 		httpsoInformer.Informer().Run(ctx.Done())
 		return nil
 	})
 
 	eg.Go(func() error {
-		lggr.Info("starting the queue pinger")
+		setupLog.Info("starting the queue pinger")
 
 		if err := pinger.start(ctx, time.NewTicker(cfg.QueueTickDuration), endpInformer); !util.IsIgnoredErr(err) {
-			lggr.Error(err, "queue pinger failed")
+			setupLog.Error(err, "queue pinger failed")
 			return err
 		}
 
@@ -116,24 +123,24 @@ func main() {
 	})
 
 	eg.Go(func() error {
-		lggr.Info("starting the grpc server")
+		setupLog.Info("starting the grpc server")
 
-		if err := startGrpcServer(ctx, cfg, lggr, grpcPort, pinger, httpsoInformer, int64(targetPendingRequests)); !util.IsIgnoredErr(err) {
-			lggr.Error(err, "grpc server failed")
+		if err := startGrpcServer(ctx, cfg, ctrl.Log, grpcPort, pinger, httpsoInformer, int64(targetPendingRequests)); !util.IsIgnoredErr(err) {
+			setupLog.Error(err, "grpc server failed")
 			return err
 		}
 
 		return nil
 	})
 
-	build.PrintComponentInfo(lggr, "Scaler")
+	build.PrintComponentInfo(ctrl.Log, "Scaler")
 
 	if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {
-		lggr.Error(err, "fatal error")
+		setupLog.Error(err, "fatal error")
 		os.Exit(1)
 	}
 
-	lggr.Info("Bye!")
+	setupLog.Info("Bye!")
 }
 
 func startGrpcServer(
diff --git a/scaler/queue_pinger.go b/scaler/queue_pinger.go
index 6500201..9320bdc 100644
--- a/scaler/queue_pinger.go
+++ b/scaler/queue_pinger.go
@@ -49,8 +49,7 @@ type queuePinger struct {
 	adminPort              string
 	pingMut                *sync.RWMutex
 	lastPingTime           time.Time
-	allCounts              map[string]int
-	aggregateCount         int
+	allCounts              map[string]queue.Count
 	lggr                   logr.Logger
 	status                 PingerStatus
 }
@@ -72,8 +71,7 @@ func newQueuePinger(
 		adminPort:              adminPort,
 		pingMut:                pingMut,
 		lggr:                   lggr,
-		allCounts:              map[string]int{},
-		aggregateCount:         0,
+		allCounts:              map[string]queue.Count{},
 	}
 	return pinger
 }
@@ -123,7 +121,7 @@ func (q *queuePinger) start(
 	}
 }
 
-func (q *queuePinger) counts() map[string]int {
+func (q *queuePinger) counts() map[string]queue.Count {
 	q.pingMut.RLock()
 	defer q.pingMut.RUnlock()
 	return q.allCounts
@@ -134,7 +132,7 @@ func (q *queuePinger) counts() map[string]int {
 func (q *queuePinger) fetchAndSaveCounts(ctx context.Context) error {
 	q.pingMut.Lock()
 	defer q.pingMut.Unlock()
-	counts, agg, err := fetchCounts(
+	counts, err := fetchCounts(
 		ctx,
 		q.lggr,
 		q.getEndpointsFn,
@@ -149,7 +147,6 @@ func (q *queuePinger) fetchAndSaveCounts(ctx context.Context) error {
 	}
 	q.status = PingerACTIVE
 	q.allCounts = counts
-	q.aggregateCount = agg
 	q.lastPingTime = time.Now()
 
 	return nil
@@ -171,7 +168,7 @@ func fetchCounts(
 	ns,
 	svcName,
 	adminPort string,
-) (map[string]int, int, error) {
+) (map[string]queue.Count, error) {
 	lggr = lggr.WithName("queuePinger.requestCounts")
 
 	endpointURLs, err := k8s.EndpointsForService(
@@ -182,11 +179,11 @@ func fetchCounts(
 		endpointsFn,
 	)
 	if err != nil {
-		return nil, 0, err
+		return nil, err
 	}
 
 	if len(endpointURLs) == 0 {
-		return nil, 0, fmt.Errorf("there isn't any valid interceptor endpoint")
+		return nil, fmt.Errorf("there isn't any valid interceptor endpoint")
 	}
 
 	countsCh := make(chan *queue.Counts)
@@ -238,21 +235,25 @@ func fetchCounts(
 
 	if err := fetchGrp.Wait(); err != nil {
 		lggr.Error(err, "fetching all counts failed")
-		return nil, 0, err
+		return nil, err
 	}
 
-	// consume the results of the counts channel
-	agg := 0
-	totalCounts := make(map[string]int)
+	totalCounts := make(map[string]queue.Count)
 	// range through the result of each endpoint
 	for count := range countsCh {
 		// each endpoint returns a map of counts, one count
 		// per host. add up the counts for each host
 		for host, val := range count.Counts {
-			agg += val
-			totalCounts[host] += val
+			var responseCount queue.Count
+			var ok bool
+			if responseCount, ok = totalCounts[host]; !ok {
+				responseCount = queue.Count{}
+			}
+			responseCount.Concurrency += val.Concurrency
+			responseCount.RPS += val.RPS
+			totalCounts[host] = responseCount
 		}
 	}
 
-	return totalCounts, agg, nil
+	return totalCounts, nil
 }


Test patch:
diff --git a/interceptor/main_test.go b/interceptor/main_test.go
index 04e2ba7..4a39c28 100644
--- a/interceptor/main_test.go
+++ b/interceptor/main_test.go
@@ -111,7 +111,7 @@ func TestRunProxyServerCountMiddleware(t *testing.T) {
 	select {
 	case hostAndCount := <-q.ResizedCh:
 		r.Equal(namespacedName, hostAndCount.Host)
-		r.Equal(+1, hostAndCount.Count)
+		r.Equal(1, hostAndCount.Count)
 	case <-time.After(500 * time.Millisecond):
 		r.Fail("timeout waiting for +1 queue resize")
 	}
@@ -126,7 +126,7 @@ func TestRunProxyServerCountMiddleware(t *testing.T) {
 	select {
 	case hostAndCount := <-q.ResizedCh:
 		r.Equal(namespacedName, hostAndCount.Host)
-		r.Equal(-1, hostAndCount.Count)
+		r.Equal(1, hostAndCount.Count)
 	case <-time.After(2 * time.Second):
 		r.Fail("timeout waiting for -1 queue resize")
 	}
@@ -142,7 +142,7 @@ func TestRunProxyServerCountMiddleware(t *testing.T) {
 		"couldn't find host %s in the queue",
 		host,
 	)
-	r.Equal(0, counts[namespacedName])
+	r.Equal(0, counts[namespacedName].Concurrency)
 
 	done()
 	r.Error(g.Wait())
diff --git a/interceptor/middleware/counting_test.go b/interceptor/middleware/counting_test.go
index dab6b24..500056f 100644
--- a/interceptor/middleware/counting_test.go
+++ b/interceptor/middleware/counting_test.go
@@ -56,10 +56,9 @@ func TestCountMiddleware(t *testing.T) {
 
 	ctx := context.Background()
 
-	// for a valid request, we expect the queue to be resized twice.
+	// for a valid request, we expect the queue to be modified twice.
 	// once to mark a pending HTTP request, then a second time to remove it.
-	// by the end of both sends, resize1 + resize2 should be 0,
-	// or in other words, the queue size should be back to zero
+	// by the end of both sends, increase1 + decrease1 should be 2
 
 	// run middleware with the host in the request
 	req, err := http.NewRequest("GET", "/something", nil)
@@ -70,7 +69,7 @@ func TestCountMiddleware(t *testing.T) {
 	req = req.WithContext(reqCtx)
 	req.Host = uri.Host
 
-	agg, respRecorder := expectResizes(
+	agg, respRecorder := expectUpdates(
 		ctx,
 		t,
 		2,
@@ -86,10 +85,10 @@ func TestCountMiddleware(t *testing.T) {
 	)
 	r.Equal(http.StatusOK, respRecorder.Code)
 	r.Equal(http.StatusText(respRecorder.Code), respRecorder.Body.String())
-	r.Equal(0, agg)
+	r.Equal(2, agg)
 }
 
-// expectResizes creates a new httptest.ResponseRecorder, then passes req through
+// expectUpdates creates a new httptest.ResponseRecorder, then passes req through
 // the middleware. every time the middleware calls fakeCounter.Resize(), it calls
 // resizeCheckFn with t and the queue.HostCount that represents the resize call
 // that was made. it also maintains an aggregate delta of the counts passed to
@@ -98,7 +97,7 @@ func TestCountMiddleware(t *testing.T) {
 //
 // this function returns the aggregate and the httptest.ResponseRecorder that was
 // created and used with the middleware
-func expectResizes(
+func expectUpdates(
 	ctx context.Context,
 	t *testing.T,
 	nResizes int,
diff --git a/pkg/queue/bucketing_test.go b/pkg/queue/bucketing_test.go
new file mode 100644
index 0000000..e54476b
--- /dev/null
+++ b/pkg/queue/bucketing_test.go
@@ -0,0 +1,343 @@
+package queue
+
+import (
+	"fmt"
+	"math/rand"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+const granularity = time.Second
+
+func TestRequestsBucketsSimple(t *testing.T) {
+	trunc1 := time.Now().Truncate(1 * time.Second)
+	trunc5 := time.Now().Truncate(5 * time.Second)
+
+	type args struct {
+		time  time.Time
+		value int
+	}
+	tests := []struct {
+		name        string
+		granularity time.Duration
+		stats       []args
+		want        map[time.Time]int
+	}{{
+		name:        "granularity = 1s",
+		granularity: time.Second,
+		stats: []args{
+			{trunc1, 1.0}, // activator scale from 0.
+			{trunc1.Add(100 * time.Millisecond), 10.0}, // from scraping pod/sent by activator.
+			{trunc1.Add(1 * time.Second), 1.0},         // next bucket
+			{trunc1.Add(3 * time.Second), 1.0},         // nextnextnext bucket
+		},
+		want: map[time.Time]int{
+			trunc1:                      11.0,
+			trunc1.Add(1 * time.Second): 1.0,
+			trunc1.Add(3 * time.Second): 1.0,
+		},
+	}, {
+		name:        "granularity = 5s",
+		granularity: 5 * time.Second,
+		stats: []args{
+			{trunc5, 1.0},
+			{trunc5.Add(3 * time.Second), 11.0}, // same bucket
+			{trunc5.Add(6 * time.Second), 1.0},  // next bucket
+		},
+		want: map[time.Time]int{
+			trunc5:                      12.0,
+			trunc5.Add(5 * time.Second): 1.0,
+		},
+	}, {
+		name:        "empty",
+		granularity: time.Second,
+		stats:       []args{},
+		want:        map[time.Time]int{},
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// New implementation test.
+			buckets := NewRequestsBuckets(2*time.Minute, tt.granularity)
+			if !buckets.IsEmpty(trunc1) {
+				t.Error("Unexpected non empty result")
+			}
+			for _, stat := range tt.stats {
+				buckets.Record(stat.time, stat.value)
+			}
+
+			got := make(map[time.Time]int)
+			// Less time in future than our window is (2mins above), but more than any of the tests report.
+			buckets.forEachBucket(trunc1.Add(time.Minute), func(t time.Time, b int) {
+				// Since we're storing 0s when there's no data, we need to exclude those
+				// for this test.
+				if b > 0 {
+					got[t] = b
+				}
+			})
+
+			if !cmp.Equal(tt.want, got) {
+				t.Error("Unexpected values (-want +got):", cmp.Diff(tt.want, got))
+			}
+		})
+	}
+}
+
+func TestRequestsBucketsManyReps(t *testing.T) {
+	trunc1 := time.Now().Truncate(granularity)
+	buckets := NewRequestsBuckets(time.Minute*5, granularity)
+	for p := 0; p < 5; p++ {
+		trunc1 = trunc1.Add(granularity)
+		for t := 0; t < 5; t++ {
+			buckets.Record(trunc1, t+p)
+		}
+	}
+	// So the buckets are:
+	// t0: [0, 1, 2, 3, 4] = 10
+	// t1: [1, 2, 3, 4, 5] = 15
+	// t2: [2, 3, 4, 5, 6] = 20
+	// t3: [3, 4, 5, 6, 7] = 25
+	// t4: [4, 5, 6, 7, 8] = 30
+	//                     = 100
+	const want = 100
+	sum1, sum2 := 0, 0
+	buckets.forEachBucket(trunc1, func(_ time.Time, b int) {
+		sum1 += b
+	})
+	buckets.forEachBucket(trunc1, func(_ time.Time, b int) {
+		sum2 += b
+	})
+	if got, want := sum1, want; got != want {
+		t.Errorf("Sum1 = %d, want: %d", got, want)
+	}
+
+	if got, want := sum2, want; got != want {
+		t.Errorf("Sum2 = %d, want: %d", got, want)
+	}
+}
+
+func TestRequestsBucketsManyRepsWithNonMonotonicalOrder(t *testing.T) {
+	start := time.Now().Truncate(granularity)
+	end := start
+	buckets := NewRequestsBuckets(time.Minute, granularity)
+
+	d := []int{0, 3, 2, 1, 4}
+	for p := 0; p < 5; p++ {
+		end = start.Add(time.Duration(d[p]) * granularity)
+		for t := 0; t < 5; t++ {
+			buckets.Record(end, p+t)
+		}
+	}
+
+	// So the buckets are:
+	// t0: [0, 1, 2, 3, 4] = 10
+	// t1: [3, 4, 5, 6, 7] = 25
+	// t2: [2, 3, 4, 5, 6] = 20
+	// t3: [1, 2, 3, 4, 5] = 15
+	// t4: [4, 5, 6, 7, 8] = 30
+	//                     = 100
+	const want = 100
+	sum1, sum2 := 0, 0
+	buckets.forEachBucket(end, func(_ time.Time, b int) {
+		sum1 += b
+	})
+	buckets.forEachBucket(end, func(_ time.Time, b int) {
+		sum2 += b
+	})
+	if got, want := sum1, want; got != want {
+		t.Errorf("Sum1 = %d, want: %d", got, want)
+	}
+
+	if got, want := sum2, want; got != want {
+		t.Errorf("Sum2 = %d, want: %d", got, want)
+	}
+}
+
+func TestRequestsBucketsWindowAverage(t *testing.T) {
+	now := time.Now()
+	buckets := NewRequestsBuckets(5*time.Second, granularity)
+
+	// This verifies that we properly use firstWrite. Without that we'd get 0.2.
+	buckets.Record(now, 1)
+	if got, want := buckets.WindowAverage(now), 1.; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+	for i := 1; i < 5; i++ {
+		buckets.Record(now.Add(time.Duration(i)*time.Second), i+1)
+	}
+
+	if got, want := buckets.WindowAverage(now.Add(4*time.Second)), 15./5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+	// Check when `now` lags behind.
+	if got, want := buckets.WindowAverage(now.Add(3600*time.Millisecond)), 15./5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Check with short hole.
+	if got, want := buckets.WindowAverage(now.Add(6*time.Second)), (15.-1-2)/(5-2); got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Check with a long hole.
+	if got, want := buckets.WindowAverage(now.Add(10*time.Second)), 0.; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Check write with holes.
+	buckets.Record(now.Add(6*time.Second), 91)
+	if got, want := buckets.WindowAverage(now.Add(6*time.Second)), (15.-1-2+91)/5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Advance much farther.
+	now = now.Add(time.Minute)
+	buckets.Record(now, 1984)
+	if got, want := buckets.WindowAverage(now), 1984.; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Check with an earlier time.
+	buckets.Record(now.Add(-3*time.Second), 4)
+	if got, want := buckets.WindowAverage(now), (4.+1984)/4; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// One more second pass.
+	now = now.Add(time.Second)
+	buckets.Record(now, 5)
+	if got, want := buckets.WindowAverage(now), (4.+1984+5)/5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Insert an earlier time again.
+	buckets.Record(now.Add(-3*time.Second), 10)
+	if got, want := buckets.WindowAverage(now), (4.+10+1984+5)/5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Verify that we ignore the value which is too early.
+	buckets.Record(now.Add(-6*time.Second), 10)
+	if got, want := buckets.WindowAverage(now), (4.+10+1984+5)/5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Verify that we ignore the value with bound timestamp.
+	buckets.Record(now.Add(-5*time.Second), 10)
+	if got, want := buckets.WindowAverage(now), (4.+10+1984+5)/5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+
+	// Verify we clear up the data when not receiving data for exact `window` peroid.
+	buckets.Record(now.Add(5*time.Second), 10)
+	if got, want := buckets.WindowAverage(now.Add(5*time.Second)), 10.; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+}
+
+func TestDescendingRecord(t *testing.T) {
+	now := time.Now()
+	buckets := NewRequestsBuckets(5*time.Second, 1*time.Second)
+
+	for i := 8 * time.Second; i >= 0*time.Second; i -= time.Second {
+		buckets.Record(now.Add(i), 5)
+	}
+
+	if got, want := buckets.WindowAverage(now.Add(5*time.Second)), 5.; got != want {
+		// we wrote a 5 every second, and we never wrote in the same second twice,
+		// so the average _should_ be 5.
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+}
+
+func TestRequestsBucketsHoles(t *testing.T) {
+	now := time.Now()
+	buckets := NewRequestsBuckets(5*time.Second, granularity)
+
+	for i := time.Duration(0); i < 5; i++ {
+		buckets.Record(now.Add(i*time.Second), int(i+1))
+	}
+
+	sum := 0
+
+	buckets.forEachBucket(now.Add(4*time.Second),
+		func(_ time.Time, b int) {
+			sum += b
+		})
+
+	if got, want := sum, 15; got != want {
+		t.Errorf("Sum = %v, want: %v", got, want)
+	}
+	if got, want := buckets.WindowAverage(now.Add(4*time.Second)), 15./5; got != want {
+		t.Errorf("WindowAverage = %v, want: %v", got, want)
+	}
+	// Now write at 9th second. Which means that seconds
+	// 5[0], 6[1], 7[2] become 0.
+	buckets.Record(now.Add(8*time.Second), 2.)
+	// So now we have [3] = 2, [4] = 5 and sum should be 7.
+	sum = 0.
+
+	buckets.forEachBucket(now.Add(8*time.Second),
+		func(_ time.Time, b int) {
+			sum += b
+		})
+	if got, want := sum, 7; got != want {
+		t.Errorf("Sum = %v, want: %v", got, want)
+	}
+}
+
+func BenchmarkWindowAverage(b *testing.B) {
+	// Window lengths in secs.
+	for _, wl := range []int{30, 60, 120, 240, 600} {
+		b.Run(fmt.Sprintf("%v-win-len", wl), func(b *testing.B) {
+			tn := time.Now().Truncate(time.Second) // To simplify everything.
+			buckets := NewRequestsBuckets(time.Duration(wl)*time.Second,
+				time.Second /*granularity*/)
+			// Populate with some random data.
+			for i := 0; i < wl; i++ {
+				buckets.Record(tn.Add(time.Duration(i)*time.Second), rand.Int()*100)
+			}
+			for i := 0; i < b.N; i++ {
+				buckets.WindowAverage(tn.Add(time.Duration(wl) * time.Second))
+			}
+		})
+	}
+}
+
+func TestRoundToNDigits(t *testing.T) {
+	if got, want := roundToNDigits(6, 3.6e-17), 0.; got != want {
+		t.Errorf("Rounding = %v, want: %v", got, want)
+	}
+	if got, want := roundToNDigits(3, 0.0004), 0.; got != want {
+		t.Errorf("Rounding = %v, want: %v", got, want)
+	}
+	if got, want := roundToNDigits(3, 1.2345), 1.234; got != want {
+		t.Errorf("Rounding = %v, want: %v", got, want)
+	}
+	if got, want := roundToNDigits(4, 1.2345), 1.2345; got != want {
+		t.Errorf("Rounding = %v, want: %v", got, want)
+	}
+	if got, want := roundToNDigits(6, 12345), 12345.; got != want {
+		t.Errorf("Rounding = %v, want: %v", got, want)
+	}
+}
+
+func (t *RequestsBuckets) forEachBucket(now time.Time, acc func(time time.Time, bucket int)) {
+	now = now.Truncate(t.granularity)
+	t.bucketsMutex.RLock()
+	defer t.bucketsMutex.RUnlock()
+
+	// So number of buckets we can process is len(buckets)-(now-lastWrite)/granularity.
+	// Since empty check above failed, we know this is at least 1 bucket.
+	numBuckets := len(t.buckets) - int(now.Sub(t.lastWrite)/t.granularity)
+	bucketTime := t.lastWrite // Always aligned with granularity.
+	si := t.timeToIndex(bucketTime)
+	for i := 0; i < numBuckets; i++ {
+		tIdx := si % len(t.buckets)
+		acc(bucketTime, t.buckets[tIdx])
+		si--
+		bucketTime = bucketTime.Add(-t.granularity)
+	}
+}
diff --git a/pkg/queue/queue_counts_test.go b/pkg/queue/queue_counts_test.go
index 6e877b5..d4ae475 100644
--- a/pkg/queue/queue_counts_test.go
+++ b/pkg/queue/queue_counts_test.go
@@ -9,15 +9,31 @@ import (
 func TestAggregate(t *testing.T) {
 	r := require.New(t)
 	counts := NewCounts()
-	counts.Counts = map[string]int{
-		"host1": 123,
-		"host2": 234,
-		"host3": 456,
-		"host4": 567,
+	counts.Counts = map[string]Count{
+		"host1": {
+			Concurrency: 123,
+			RPS:         123,
+		},
+		"host2": {
+			Concurrency: 234,
+			RPS:         234,
+		},
+		"host3": {
+			Concurrency: 345,
+			RPS:         345,
+		},
+		"host4": {
+			Concurrency: 456,
+			RPS:         456,
+		},
 	}
-	expectedAgg := 0
+	expectedConcurrency := 0
+	expectedRPS := 0.
 	for _, v := range counts.Counts {
-		expectedAgg += v
+		expectedConcurrency += v.Concurrency
+		expectedRPS += v.RPS
 	}
-	r.Equal(expectedAgg, counts.Aggregate())
+	agg := counts.Aggregate()
+	r.Equal(expectedConcurrency, agg.Concurrency)
+	r.Equal(expectedRPS, agg.RPS)
 }
diff --git a/pkg/queue/queue_rpc_test.go b/pkg/queue/queue_rpc_test.go
index e865ba6..f3053fc 100644
--- a/pkg/queue/queue_rpc_test.go
+++ b/pkg/queue/queue_rpc_test.go
@@ -16,21 +16,23 @@ func TestQueueSizeHandlerSuccess(t *testing.T) {
 	lggr := logr.Discard()
 	r := require.New(t)
 	reader := &FakeCountReader{
-		current: 123,
-		err:     nil,
+		concurrency: 123,
+		rps:         100,
+		err:         nil,
 	}
 
 	handler := newSizeHandler(lggr, reader)
 	req, rec := pkghttp.NewTestCtx("GET", "/queue")
 	handler.ServeHTTP(rec, req)
 	r.Equal(200, rec.Code, "response code")
-	respMap := map[string]int{}
+	respMap := map[string]Count{}
 	decodeErr := json.NewDecoder(rec.Body).Decode(&respMap)
 	r.NoError(decodeErr)
 	r.Equalf(1, len(respMap), "response JSON length was not 1")
 	sizeVal, ok := respMap["sample.com"]
 	r.Truef(ok, "'sample.com' entry not available in return JSON")
-	r.Equalf(reader.current, sizeVal, "returned JSON queue size was wrong")
+	r.Equalf(reader.concurrency, sizeVal.Concurrency, "returned JSON concurrent size was wrong")
+	r.Equalf(reader.rps, sizeVal.RPS, "returned JSON rps size was wrong")
 
 	reader.err = errors.New("test error")
 	req, rec = pkghttp.NewTestCtx("GET", "/queue")
@@ -42,8 +44,9 @@ func TestQueueSizeHandlerFail(t *testing.T) {
 	lggr := logr.Discard()
 	r := require.New(t)
 	reader := &FakeCountReader{
-		current: 0,
-		err:     errors.New("test error"),
+		concurrency: 0,
+		rps:         0,
+		err:         errors.New("test error"),
 	}
 
 	handler := newSizeHandler(lggr, reader)
@@ -56,8 +59,9 @@ func TestQueueSizeHandlerIntegration(t *testing.T) {
 	lggr := logr.Discard()
 	r := require.New(t)
 	reader := &FakeCountReader{
-		current: 50,
-		err:     nil,
+		concurrency: 50,
+		rps:         60,
+		err:         nil,
 	}
 
 	hdl := kedanet.NewTestHTTPHandlerWrapper(newSizeHandler(lggr, reader))
@@ -69,7 +73,8 @@ func TestQueueSizeHandlerIntegration(t *testing.T) {
 	r.NoError(err)
 	r.Equal(1, len(counts.Counts))
 	for _, val := range counts.Counts {
-		r.Equal(reader.current, val)
+		r.Equal(reader.concurrency, val.Concurrency)
+		r.Equal(reader.rps, val.RPS)
 	}
 	reqs := hdl.IncomingRequests()
 	r.Equal(1, len(reqs))
diff --git a/pkg/queue/queue_test.go b/pkg/queue/queue_test.go
index e8212a9..e5d1270 100644
--- a/pkg/queue/queue_test.go
+++ b/pkg/queue/queue_test.go
@@ -2,6 +2,7 @@ package queue
 
 import (
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/require"
 )
@@ -9,15 +10,20 @@ import (
 func TestCurrent(t *testing.T) {
 	r := require.New(t)
 	memory := NewMemory()
-	err := memory.Resize("host1", 1)
+	now := time.Now()
+	host := "host1"
+	memory.EnsureKey(host, time.Minute, time.Second)
+	err := memory.Increase(host, 1)
 	r.NoError(err)
 	current, err := memory.Current()
 	r.NoError(err)
-	r.Equal(current.Counts, memory.countMap)
+	r.Equal(current.Counts[host].Concurrency, memory.concurrentMap[host])
+	r.Equal(current.Counts[host].RPS, memory.rpsMap[host].WindowAverage(now))
 
-	err = memory.Resize("host1", 1)
+	err = memory.Increase(host, 1)
 	r.NoError(err)
-	err = memory.Resize("host2", 1)
+	err = memory.Increase(host, 1)
 	r.NoError(err)
-	r.NotEqual(current.Counts, memory.countMap)
+	r.NotEqual(current.Counts[host].Concurrency, memory.concurrentMap[host])
+	r.NotEqual(current.Counts[host].RPS, memory.rpsMap[host].WindowAverage(now))
 }
diff --git a/pkg/routing/table_test.go b/pkg/routing/table_test.go
index fe36aa7..9cbce19 100644
--- a/pkg/routing/table_test.go
+++ b/pkg/routing/table_test.go
@@ -16,6 +16,7 @@ import (
 	clientsethttpv1alpha1mock "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/typed/http/v1alpha1/mock"
 	informersexternalversions "github.com/kedacore/http-add-on/operator/generated/informers/externalversions"
 	"github.com/kedacore/http-add-on/pkg/k8s"
+	"github.com/kedacore/http-add-on/pkg/queue"
 	"github.com/kedacore/http-add-on/pkg/util"
 )
 
@@ -110,7 +111,7 @@ var _ = Describe("Table", func() {
 
 	Context("New", func() {
 		It("returns a table with fields initialized", func() {
-			i, err := NewTable(sharedInformerFactory, namespace)
+			i, err := NewTable(sharedInformerFactory, namespace, queue.NewFakeCounter())
 			Expect(err).NotTo(HaveOccurred())
 			Expect(i).NotTo(BeNil())
 
@@ -135,7 +136,7 @@ var _ = Describe("Table", func() {
 		)
 
 		BeforeEach(func() {
-			i, _ := NewTable(sharedInformerFactory, namespace)
+			i, _ := NewTable(sharedInformerFactory, namespace, queue.NewFakeCounter())
 			t = i.(*table)
 		})
 
@@ -180,7 +181,7 @@ var _ = Describe("Table", func() {
 		)
 
 		BeforeEach(func() {
-			i, _ := NewTable(sharedInformerFactory, namespace)
+			i, _ := NewTable(sharedInformerFactory, namespace, queue.NewFakeCounter())
 			t = i.(*table)
 		})
 
@@ -278,7 +279,7 @@ var _ = Describe("Table", func() {
 		)
 
 		BeforeEach(func() {
-			i, _ := NewTable(sharedInformerFactory, namespace)
+			i, _ := NewTable(sharedInformerFactory, namespace, queue.NewFakeCounter())
 			t = i.(*table)
 		})
 
diff --git a/scaler/handlers_test.go b/scaler/handlers_test.go
index a3689e8..6208b30 100644
--- a/scaler/handlers_test.go
+++ b/scaler/handlers_test.go
@@ -26,6 +26,8 @@ import (
 	"github.com/kedacore/http-add-on/pkg/queue"
 )
 
+const validHTTPScaledObjectName = "valid-httpscaledobject"
+
 func TestStreamIsActive(t *testing.T) {
 	type testCase struct {
 		name           string
@@ -33,6 +35,7 @@ func TestStreamIsActive(t *testing.T) {
 		expectedErr    bool
 		setup          func(t *testing.T, qp *queuePinger)
 		scalerMetadata map[string]string
+		scalingMetric  *httpv1alpha1.ScalingMetricSpec
 	}
 	testCases := []testCase{
 		{
@@ -49,11 +52,14 @@ func TestStreamIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts[key] = 0
+				qp.allCounts[key] = queue.Count{}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
 			},
 		},
 		{
-			name:        "Simple host active",
+			name:        "Simple host active by concurrency and scaling metric nil",
 			expected:    true,
 			expectedErr: false,
 			setup: func(t *testing.T, qp *queuePinger) {
@@ -66,11 +72,17 @@ func TestStreamIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts[key] = 1
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 1,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
 			},
+			scalingMetric: nil,
 		},
 		{
-			name:        "Simple multi host active",
+			name:        "Simple host active by concurrency and concurrency scaling metric",
 			expected:    true,
 			expectedErr: false,
 			setup: func(t *testing.T, qp *queuePinger) {
@@ -83,14 +95,113 @@ func TestStreamIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts[key] = 2
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 1,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
+			scalingMetric: &httpv1alpha1.ScalingMetricSpec{
+				Concurrency: &httpv1alpha1.ConcurrencyMetricSpec{},
 			},
 		},
 		{
-			name:        "No host present, but host in routing table",
+			name:        "Simple host active by concurrency and concurrency scaling rate",
 			expected:    false,
 			expectedErr: false,
-			setup:       func(_ *testing.T, _ *queuePinger) {},
+			setup: func(t *testing.T, qp *queuePinger) {
+				namespacedName := &types.NamespacedName{
+					Namespace: "default",
+					Name:      t.Name(),
+				}
+				key := namespacedName.String()
+
+				qp.pingMut.Lock()
+				defer qp.pingMut.Unlock()
+
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 1,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
+			scalingMetric: &httpv1alpha1.ScalingMetricSpec{
+				Rate: &httpv1alpha1.RateMetricSpec{},
+			},
+		},
+		{
+			name:        "Simple host active by rate and concurrency scaling metric",
+			expected:    false,
+			expectedErr: false,
+			setup: func(t *testing.T, qp *queuePinger) {
+				namespacedName := &types.NamespacedName{
+					Namespace: "default",
+					Name:      t.Name(),
+				}
+				key := namespacedName.String()
+
+				qp.pingMut.Lock()
+				defer qp.pingMut.Unlock()
+
+				qp.allCounts[key] = queue.Count{
+					RPS: 1,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
+			scalingMetric: &httpv1alpha1.ScalingMetricSpec{
+				Concurrency: &httpv1alpha1.ConcurrencyMetricSpec{},
+			},
+		},
+		{
+			name:        "Simple host active by concurrency and concurrency scaling rate",
+			expected:    true,
+			expectedErr: false,
+			setup: func(t *testing.T, qp *queuePinger) {
+				namespacedName := &types.NamespacedName{
+					Namespace: "default",
+					Name:      t.Name(),
+				}
+				key := namespacedName.String()
+
+				qp.pingMut.Lock()
+				defer qp.pingMut.Unlock()
+
+				qp.allCounts[key] = queue.Count{
+					RPS: 1,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
+			scalingMetric: &httpv1alpha1.ScalingMetricSpec{
+				Rate: &httpv1alpha1.RateMetricSpec{},
+			},
+		},
+		{
+			name:        "Simple multi host active",
+			expected:    true,
+			expectedErr: false,
+			setup: func(t *testing.T, qp *queuePinger) {
+				namespacedName := &types.NamespacedName{
+					Namespace: "default",
+					Name:      t.Name(),
+				}
+				key := namespacedName.String()
+
+				qp.pingMut.Lock()
+				defer qp.pingMut.Unlock()
+
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 2,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
 		},
 		{
 			name:        "Host doesn't exist",
@@ -106,9 +217,15 @@ func TestStreamIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts["a"] = 1
-				qp.allCounts["b"] = 2
-				qp.allCounts["c"] = 3
+				qp.allCounts["a"] = queue.Count{
+					Concurrency: 1,
+				}
+				qp.allCounts["b"] = queue.Count{
+					Concurrency: 2,
+				}
+				qp.allCounts["c"] = queue.Count{
+					Concurrency: 3,
+				}
 			},
 			scalerMetadata: map[string]string{
 				keyInterceptorTargetPendingRequests: "1000",
@@ -124,7 +241,7 @@ func TestStreamIsActive(t *testing.T) {
 			r := require.New(t)
 			ctx := context.Background()
 			lggr := logr.Discard()
-			informer, _, _ := newMocks(ctrl)
+			informer, _, _ := newMocks(ctrl, tc.scalingMetric)
 			ticker, pinger, err := newFakeQueuePinger(lggr)
 			r.NoError(err)
 			defer ticker.Stop()
@@ -215,7 +332,12 @@ func TestIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts[key] = 0
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 0,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
 			},
 		},
 		{
@@ -232,7 +354,12 @@ func TestIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts[key] = 1
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 1,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
 			},
 		},
 		{
@@ -249,14 +376,13 @@ func TestIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts[key] = 2
+				qp.allCounts[key] = queue.Count{
+					Concurrency: 2,
+				}
+			},
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
 			},
-		},
-		{
-			name:        "No host present, but host in routing table",
-			expected:    false,
-			expectedErr: false,
-			setup:       func(_ *testing.T, _ *queuePinger) {},
 		},
 		{
 			name:        "Host doesn't exist",
@@ -272,9 +398,15 @@ func TestIsActive(t *testing.T) {
 				qp.pingMut.Lock()
 				defer qp.pingMut.Unlock()
 
-				qp.allCounts["a"] = 1
-				qp.allCounts["b"] = 2
-				qp.allCounts["c"] = 3
+				qp.allCounts["a"] = queue.Count{
+					Concurrency: 1,
+				}
+				qp.allCounts["b"] = queue.Count{
+					Concurrency: 2,
+				}
+				qp.allCounts["c"] = queue.Count{
+					Concurrency: 3,
+				}
 			},
 			scalerMetadata: map[string]string{
 				keyInterceptorTargetPendingRequests: "1000",
@@ -290,7 +422,7 @@ func TestIsActive(t *testing.T) {
 			r := require.New(t)
 			ctx := context.Background()
 			lggr := logr.Discard()
-			informer, _, _ := newMocks(ctrl)
+			informer, _, _ := newMocks(ctrl, nil)
 			ticker, pinger, err := newFakeQueuePinger(lggr)
 			r.NoError(err)
 			defer ticker.Stop()
@@ -338,7 +470,7 @@ func TestGetMetricSpecTable(t *testing.T) {
 			name:                "valid host as single host value in scaler metadata",
 			defaultTargetMetric: 0,
 			newInformer: func(t *testing.T, ctrl *gomock.Controller) *informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer {
-				informer, _, namespaceLister := newMocks(ctrl)
+				informer, _, namespaceLister := newMocks(ctrl, nil)
 
 				httpso := &httpv1alpha1.HTTPScaledObject{
 					ObjectMeta: metav1.ObjectMeta{
@@ -375,7 +507,7 @@ func TestGetMetricSpecTable(t *testing.T) {
 			name:                "valid hosts as multiple hosts value in scaler metadata",
 			defaultTargetMetric: 0,
 			newInformer: func(t *testing.T, ctrl *gomock.Controller) *informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer {
-				informer, _, namespaceLister := newMocks(ctrl)
+				informer, _, namespaceLister := newMocks(ctrl, nil)
 
 				httpso := &httpv1alpha1.HTTPScaledObject{
 					ObjectMeta: metav1.ObjectMeta{
@@ -416,7 +548,7 @@ func TestGetMetricSpecTable(t *testing.T) {
 			name:                "interceptor",
 			defaultTargetMetric: 0,
 			newInformer: func(t *testing.T, ctrl *gomock.Controller) *informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer {
-				informer, _, namespaceLister := newMocks(ctrl)
+				informer, _, namespaceLister := newMocks(ctrl, nil)
 
 				namespaceLister.EXPECT().
 					Get(gomock.Any()).
@@ -509,7 +641,9 @@ func TestGetMetrics(t *testing.T) {
 		for host, val := range hostMap {
 			// NOTE: don't call .Resize here or you'll have to make sure
 			// to receive on q.ResizedCh
-			q.RetMap[host] = val
+			q.RetMap[host] = queue.Count{
+				Concurrency: val,
+			}
 		}
 		// create the HTTP server to encode and serve
 		// the host map
@@ -555,7 +689,7 @@ func TestGetMetrics(t *testing.T) {
 				ctx context.Context,
 				lggr logr.Logger,
 			) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) {
-				informer, _, _ := newMocks(ctrl)
+				informer, _, _ := newMocks(ctrl, nil)
 
 				// create queue and ticker without the host in it
 				ticker, pinger, err := newFakeQueuePinger(lggr)
@@ -576,6 +710,9 @@ func TestGetMetrics(t *testing.T) {
 				r.Equal(int64(0), metricVal.MetricValue)
 			},
 			defaultTargetMetric: int64(200),
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
 		},
 		{
 			name: "HTTPSO present in the queue pinger",
@@ -585,7 +722,7 @@ func TestGetMetrics(t *testing.T) {
 				ctx context.Context,
 				lggr logr.Logger,
 			) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) {
-				informer, _, _ := newMocks(ctrl)
+				informer, _, _ := newMocks(ctrl, nil)
 
 				namespacedName := &types.NamespacedName{
 					Namespace: ns,
@@ -613,6 +750,9 @@ func TestGetMetrics(t *testing.T) {
 				r.Equal(int64(201), metricVal.MetricValue)
 			},
 			defaultTargetMetric: int64(200),
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
 		},
 		{
 			name: "multiple validHosts add MetricValues",
@@ -622,7 +762,7 @@ func TestGetMetrics(t *testing.T) {
 				ctx context.Context,
 				lggr logr.Logger,
 			) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) {
-				informer, _, _ := newMocks(ctrl)
+				informer, _, _ := newMocks(ctrl, nil)
 
 				namespacedName := &types.NamespacedName{
 					Namespace: ns,
@@ -653,6 +793,9 @@ func TestGetMetrics(t *testing.T) {
 				r.Equal(int64(579), metricVal.MetricValue)
 			},
 			defaultTargetMetric: int64(500),
+			scalerMetadata: map[string]string{
+				k8s.HTTPScaledObjectKey: validHTTPScaledObjectName,
+			},
 		},
 		{
 			name: "interceptor",
@@ -662,7 +805,7 @@ func TestGetMetrics(t *testing.T) {
 				ctx context.Context,
 				lggr logr.Logger,
 			) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) {
-				informer, _, _ := newMocks(ctrl)
+				informer, _, _ := newMocks(ctrl, nil)
 
 				memory := map[string]int{
 					"a": 1,
@@ -730,11 +873,21 @@ func TestGetMetrics(t *testing.T) {
 	}
 }
 
-func newMocks(ctrl *gomock.Controller) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *listershttpv1alpha1mock.MockHTTPScaledObjectLister, *listershttpv1alpha1mock.MockHTTPScaledObjectNamespaceLister) {
+func newMocks(ctrl *gomock.Controller, scalingMetric *httpv1alpha1.ScalingMetricSpec) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *listershttpv1alpha1mock.MockHTTPScaledObjectLister, *listershttpv1alpha1mock.MockHTTPScaledObjectNamespaceLister) {
 	namespaceLister := listershttpv1alpha1mock.NewMockHTTPScaledObjectNamespaceLister(ctrl)
 	namespaceLister.EXPECT().
-		Get("").
+		Get(validHTTPScaledObjectName).
 		DoAndReturn(func(name string) (*httpv1alpha1.HTTPScaledObject, error) {
+			if name == validHTTPScaledObjectName {
+				return &httpv1alpha1.HTTPScaledObject{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: validHTTPScaledObjectName,
+					},
+					Spec: httpv1alpha1.HTTPScaledObjectSpec{
+						ScalingMetric: scalingMetric,
+					},
+				}, nil
+			}
 			return nil, errors.NewNotFound(httpv1alpha1.Resource("httpscaledobject"), name)
 		}).
 		AnyTimes()
diff --git a/scaler/queue_pinger_test.go b/scaler/queue_pinger_test.go
index 67398e9..202bd9c 100644
--- a/scaler/queue_pinger_test.go
+++ b/scaler/queue_pinger_test.go
@@ -30,16 +30,29 @@ func TestCounts(t *testing.T) {
 
 	// assemble an in-memory queue and start up a fake server that serves it.
 	// we'll configure the queue pinger to use that server below
-	counts := map[string]int{
-		"host1": 123,
-		"host2": 234,
-		"host3": 456,
-		"host4": 809,
+	counts := map[string]queue.Count{
+		"host1": {
+			Concurrency: 123,
+			RPS:         123,
+		},
+		"host2": {
+			Concurrency: 234,
+			RPS:         234,
+		},
+		"host3": {
+			Concurrency: 345,
+			RPS:         345,
+		},
+		"host4": {
+			Concurrency: 456,
+			RPS:         456,
+		},
 	}
 
 	q := queue.NewMemory()
 	for host, count := range counts {
-		r.NoError(q.Resize(host, count))
+		q.EnsureKey(host, time.Minute, time.Second)
+		r.NoError(q.Increase(host, count.Concurrency))
 	}
 
 	srv, srvURL, endpoints, err := startFakeQueueEndpointServer(
@@ -85,7 +98,8 @@ func TestCounts(t *testing.T) {
 
 		// note that the returned value should be:
 		// (queue_count * num_endpoints)
-		r.Equal(count*3, retCount)
+		r.Equal(count.Concurrency*3, retCount.Concurrency)
+		r.Equal(count.RPS*3, retCount.RPS)
 	}
 }
 
@@ -101,14 +115,28 @@ func TestFetchAndSaveCounts(t *testing.T) {
 		numEndpoints = 3
 	)
 	counts := queue.NewCounts()
-	counts.Counts = map[string]int{
-		"host1": 123,
-		"host2": 234,
-		"host3": 345,
+	counts.Counts = map[string]queue.Count{
+		"host1": {
+			Concurrency: 123,
+			RPS:         123,
+		},
+		"host2": {
+			Concurrency: 234,
+			RPS:         234,
+		},
+		"host3": {
+			Concurrency: 345,
+			RPS:         345,
+		},
+		"host4": {
+			Concurrency: 456,
+			RPS:         456,
+		},
 	}
 	q := queue.NewMemory()
 	for host, count := range counts.Counts {
-		r.NoError(q.Resize(host, count))
+		q.EnsureKey(host, time.Minute, time.Second)
+		r.NoError(q.Increase(host, count.Concurrency))
 	}
 	srv, srvURL, endpoints, err := startFakeQueueEndpointServer(
 		svcName, q, numEndpoints,
@@ -130,21 +158,18 @@ func TestFetchAndSaveCounts(t *testing.T) {
 		svcName,
 		deplName,
 		srvURL.Port(),
-		// time.NewTicker(1*time.Millisecond),
 	)
 
 	r.NoError(pinger.fetchAndSaveCounts(ctx))
 
 	// since all endpoints serve the same counts,
-	// expected aggregate is individual count * # endpoints
-	expectedAgg := counts.Aggregate() * numEndpoints
-	r.Equal(expectedAgg, pinger.aggregateCount)
-	// again, since all endpoints serve the same counts,
 	// the hosts will be the same as the original counts,
 	// but the value is (individual count * # endpoints)
 	expectedCounts := counts.Counts
 	for host, val := range expectedCounts {
-		expectedCounts[host] = val * numEndpoints
+		val.Concurrency *= 3
+		val.RPS *= 3
+		expectedCounts[host] = val
 	}
 	r.Equal(expectedCounts, pinger.allCounts)
 }
@@ -160,14 +185,28 @@ func TestFetchCounts(t *testing.T) {
 		numEndpoints = 3
 	)
 	counts := queue.NewCounts()
-	counts.Counts = map[string]int{
-		"host1": 123,
-		"host2": 234,
-		"host3": 345,
+	counts.Counts = map[string]queue.Count{
+		"host1": {
+			Concurrency: 123,
+			RPS:         123,
+		},
+		"host2": {
+			Concurrency: 234,
+			RPS:         234,
+		},
+		"host3": {
+			Concurrency: 345,
+			RPS:         345,
+		},
+		"host4": {
+			Concurrency: 456,
+			RPS:         456,
+		},
 	}
 	q := queue.NewMemory()
 	for host, count := range counts.Counts {
-		r.NoError(q.Resize(host, count))
+		q.EnsureKey(host, time.Minute, time.Second)
+		r.NoError(q.Increase(host, count.Concurrency))
 	}
 	srv, srvURL, endpoints, err := startFakeQueueEndpointServer(
 		svcName, q, numEndpoints,
@@ -183,7 +222,7 @@ func TestFetchCounts(t *testing.T) {
 		return endpoints, nil
 	}
 
-	cts, agg, err := fetchCounts(
+	cts, err := fetchCounts(
 		ctx,
 		logr.Discard(),
 		endpointsFn,
@@ -192,16 +231,15 @@ func TestFetchCounts(t *testing.T) {
 		srvURL.Port(),
 	)
 	r.NoError(err)
+
 	// since all endpoints serve the same counts,
-	// expected aggregate is individual count * # endpoints
-	expectedAgg := counts.Aggregate() * numEndpoints
-	r.Equal(expectedAgg, agg)
-	// again, since all endpoints serve the same counts,
 	// the hosts will be the same as the original counts,
 	// but the value is (individual count * # endpoints)
 	expectedCounts := counts.Counts
 	for host, val := range expectedCounts {
-		expectedCounts[host] = val * numEndpoints
+		val.Concurrency *= 3
+		val.RPS *= 3
+		expectedCounts[host] = val
 	}
 	r.Equal(expectedCounts, cts)
 }
diff --git a/tests/checks/multiple_hosts/multiple_hosts_test.go b/tests/checks/multiple_hosts/multiple_hosts_test.go
index ed00f5a..e4f4315 100644
--- a/tests/checks/multiple_hosts/multiple_hosts_test.go
+++ b/tests/checks/multiple_hosts/multiple_hosts_test.go
@@ -121,8 +121,10 @@ spec:
   hosts:
   - {{.Host1}}
   - {{.Host2}}
-  targetPendingRequests: 100
   scaledownPeriod: 10
+  scalingMetric:
+    concurrency:
+      targetValue: 20
   scaleTargetRef:
     deployment: {{.DeploymentName}}
     service: {{.ServiceName}}
diff --git a/tests/checks/path_prefix/path_prefix_test.go b/tests/checks/path_prefix/path_prefix_test.go
index 8e427c4..4c11bd1 100644
--- a/tests/checks/path_prefix/path_prefix_test.go
+++ b/tests/checks/path_prefix/path_prefix_test.go
@@ -185,7 +185,9 @@ spec:
   - {{.Host}}
   pathPrefixes:
   - {{.PathPrefix2}}
-  targetPendingRequests: 100
+  scalingMetric:
+    concurrency:
+      targetValue: 20
   scaledownPeriod: 10
   scaleTargetRef:
     deployment: {{.DeploymentName}}-2
diff --git a/tests/checks/scaling_phase_custom_resource/scaling_phase_custom_resource_test.go b/tests/checks/scaling_phase_custom_resource/scaling_phase_custom_resource_test.go
index e2c9896..3d97ccf 100644
--- a/tests/checks/scaling_phase_custom_resource/scaling_phase_custom_resource_test.go
+++ b/tests/checks/scaling_phase_custom_resource/scaling_phase_custom_resource_test.go
@@ -113,7 +113,7 @@ spec:
           - "-n"
           - "2000000"
           - "-c"
-          - "20"
+          - "5"
           - "-H"
           - "Host: {{.Host}}"
           - "http://keda-http-add-on-interceptor-proxy.keda:8080/"
@@ -131,8 +131,12 @@ metadata:
 spec:
   hosts:
   - {{.Host}}
-  targetPendingRequests: 2
-  scaledownPeriod: 10
+  scalingMetric:
+    requestRate:
+      granularity: 1s
+      targetValue: 20
+      window: 1m
+  scaledownPeriod: 0
   scaleTargetRef:
     name: {{.RolloutName}}
     apiVersion: argoproj.io/v1alpha1
diff --git a/tests/checks/scaling_phase_deployment/scaling_phase_deployment_test.go b/tests/checks/scaling_phase_deployment/scaling_phase_deployment_test.go
index 58ef3bd..3ad48d4 100644
--- a/tests/checks/scaling_phase_deployment/scaling_phase_deployment_test.go
+++ b/tests/checks/scaling_phase_deployment/scaling_phase_deployment_test.go
@@ -106,7 +106,7 @@ spec:
           - "-n"
           - "2000000"
           - "-c"
-          - "20"
+          - "5"
           - "-H"
           - "Host: {{.Host}}"
           - "http://keda-http-add-on-interceptor-proxy.keda:8080/"
@@ -124,8 +124,12 @@ metadata:
 spec:
   hosts:
   - {{.Host}}
-  targetPendingRequests: 2
-  scaledownPeriod: 10
+  scalingMetric:
+    requestRate:
+      granularity: 1s
+      targetValue: 20
+      window: 1m
+  scaledownPeriod: 0
   scaleTargetRef:
     name: {{.DeploymentName}}
     service: {{.ServiceName}}
diff --git a/tests/checks/scaling_phase_deployment_with_skip_so_creation/scaling_phase_deployment_with_skip_so_creation_test.go b/tests/checks/scaling_phase_deployment_with_skip_so_creation/scaling_phase_deployment_with_skip_so_creation_test.go
index 1611143..cb97118 100644
--- a/tests/checks/scaling_phase_deployment_with_skip_so_creation/scaling_phase_deployment_with_skip_so_creation_test.go
+++ b/tests/checks/scaling_phase_deployment_with_skip_so_creation/scaling_phase_deployment_with_skip_so_creation_test.go
@@ -108,7 +108,7 @@ spec:
           - "-n"
           - "2000000"
           - "-c"
-          - "20"
+          - "5"
           - "-H"
           - "Host: {{.Host}}"
           - "http://keda-http-add-on-interceptor-proxy.keda:8080/"
@@ -128,7 +128,12 @@ metadata:
 spec:
   hosts:
   - {{.Host}}
-  scaledownPeriod: 10
+  scalingMetric:
+    requestRate:
+      granularity: 1s
+      targetValue: 20
+      window: 1m
+  scaledownPeriod: 0
   scaleTargetRef:
     name: {{.DeploymentName}}
     service: {{.ServiceName}}
@@ -156,7 +161,7 @@ spec:
   triggers:
   - type: external-push
     metadata:
-      hosts: {{.Host}}
+      httpScaledObject: {{.HTTPScaledObjectName}}
       scalerAddress: keda-http-add-on-external-scaler.keda:9090
 `
 )
diff --git a/tests/checks/scaling_phase_statefulset/scaling_phase_statefulset_test.go b/tests/checks/scaling_phase_statefulset/scaling_phase_statefulset_test.go
index e62f9e8..f0c7332 100644
--- a/tests/checks/scaling_phase_statefulset/scaling_phase_statefulset_test.go
+++ b/tests/checks/scaling_phase_statefulset/scaling_phase_statefulset_test.go
@@ -106,7 +106,7 @@ spec:
           - "-n"
           - "2000000"
           - "-c"
-          - "20"
+          - "5"
           - "-H"
           - "Host: {{.Host}}"
           - "http://keda-http-add-on-interceptor-proxy.keda:8080/"
@@ -124,8 +124,12 @@ metadata:
 spec:
   hosts:
   - {{.Host}}
-  targetPendingRequests: 2
-  scaledownPeriod: 10
+  scalingMetric:
+    requestRate:
+      granularity: 1s
+      targetValue: 20
+      window: 1m
+  scaledownPeriod: 0
   scaleTargetRef:
     name: {{.StatefulSetName}}
     kind: StatefulSet