Change:
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks! If the matter is security related, please disclose it privately via --I noticed that does not make sense to me. Looking at how LIST requests are served (which is NOT easy, there is so much dependency injection), I concluded that the worst case number of objects touched in the apiserver equals the pagination limit only if all of the following are true. The LIST request is served from etcd rather than the watch cache. The pagination FeatureGate is enabled. The request has a specified limit and it is no greater than the number of objects in etcd (restricted by namespace if relevant). The request does not have a label selector or field selector. I expected the list work estimator to use the pagination limit only under the conditions listed above. Kubernetes version (use ): Cloud provider or hardware configuration: OS (e.g: ): Kernel (e.g. ): Install tools: Network plugin and version (if this is a network-related bug): Others:
I commented on the PR.
Thank you both Mike and Wojciech! /triage accepted

Discussion:
} isListFromCache := !shouldListFromStorage(query, &listOptions) <del> count, err := e.countGetterFn(key(requestInfo)) </del> <ins> numStored, err := e.countGetterFn(key(requestInfo)) </ins> switch { case err == ObjectCountStaleErr: // object count going stale is indicative of degradation, so we should

return WorkEstimate{Seats: maximumSeats} } <del> // TODO: For resources that implement indexes at the watchcache level, //  we need to adjust the cost accordingly </del> <ins> limit := numStored if utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking) && listOptions.Limit > 0 && listOptions.Limit < numStored { limit = listOptions.Limit } </ins> var estimatedObjectsToBeProcessed int64 switch { case isListFromCache: <del> // if we are here, count is known estimatedObjectsToBeProcessed = count </del> <ins> // TODO: For resources that implement indexes at the watchcache level, //  we need to adjust the cost accordingly estimatedObjectsToBeProcessed = numStored case listOptions.FieldSelector != "" || listOptions.LabelSelector != "": estimatedObjectsToBeProcessed = numStored + limit </ins> default: <del> // Even if a selector is specified and we may need to list and go over more objects from etcd // to produce the result of size <limit>, each individual chunk will be of size at most <limit>. // As a result. the work estimate of the request should be computed based on <limit> and the actual // cost of processing more elements will be hidden in the request processing latency. estimatedObjectsToBeProcessed = listOptions.Limit if estimatedObjectsToBeProcessed == 0 { // limit has not been specified, fall back to count estimatedObjectsToBeProcessed = count } </del> <ins> estimatedObjectsToBeProcessed = 2 * limit </ins> } // for now, our rough estimate is to allocate one seat to each 100 obejcts that

seatsExpected: maximumSeats, }, { <del> name:       "request verb is list, resource version not set", requestURI: "http://server/apis/foo.bar/v1/events?limit=499", </del> <ins> name:       "request verb is list, has limit and resource version is 1", requestURI: "http://server/apis/foo.bar/v1/events?limit=399&resourceVersion=1", </ins> requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, counts: map[string]int64{ <del> "events.foo.bar": 799, </del> <ins> "events.foo.bar": 699, </ins> }, <del> seatsExpected: 5, </del> <ins> seatsExpected: 8, </ins> }, { <del> name:       "request verb is list, continuation is set", requestURI: "http://server/apis/foo.bar/v1/events?continue=token&limit=499&resourceVersion=1", </del> <ins> name:       "request verb is list, limit not set", requestURI: "http://server/apis/foo.bar/v1/events?resourceVersion=1", </ins> requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, counts: map[string]int64{ <del> "events.foo.bar": 799, </del> <ins> "events.foo.bar": 699, </ins> }, <del> seatsExpected: 5, </del> <ins> seatsExpected: 7, </ins> }, { <del> name:       "request verb is list, has limit", requestURI: "http://server/apis/foo.bar/v1/events?limit=499&resourceVersion=1", </del> <ins> name:       "request verb is list, resource version not set", requestURI: "http://server/apis/foo.bar/v1/events?limit=399", </ins> requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, counts: map[string]int64{ <del> "events.foo.bar": 799, </del> <ins> "events.foo.bar": 699, </ins> }, <del> seatsExpected: 5, </del> <ins> seatsExpected: 8, </ins> }, { <del> name:       "request verb is list, resource version is zero", requestURI: "http://server/apis/foo.bar/v1/events?resourceVersion=0", </del> <ins> name:       "request verb is list, no query parameters, count known", requestURI: "http://server/apis/foo.bar/v1/events", </ins> requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, counts: map[string]int64{ <del> "events.foo.bar": 799, </del> <ins> "events.foo.bar": 399, </ins> }, seatsExpected: 8, }, { <del> name:       "request verb is list, no query parameters, count known", </del> <ins> name:       "request verb is list, no query parameters, count not known", </ins> requestURI: "http://server/apis/foo.bar/v1/events", requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, <ins> countErr:      ObjectCountNotFoundErr, seatsExpected: maximumSeats, }, { name:       "request verb is list, continuation is set", requestURI: "http://server/apis/foo.bar/v1/events?continue=token&limit=399", requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, </ins> counts: map[string]int64{ <del> "events.foo.bar": 799, </del> <ins> "events.foo.bar": 699, </ins> }, seatsExpected: 8, }, { <del> name:       "request verb is list, no query parameters, count not known", requestURI: "http://server/apis/foo.bar/v1/events", </del> <ins> name:       "request verb is list, resource version is zero", requestURI: "http://server/apis/foo.bar/v1/events?limit=299&resourceVersion=0", </ins> requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, <del> countErr:      ObjectCountNotFoundErr, seatsExpected: maximumSeats, </del> <ins> counts: map[string]int64{ "events.foo.bar": 399, }, seatsExpected: 4, </ins> }, { <del> name:       "request verb is list, resource version match is Exact", requestURI: "http://server/apis/foo.bar/v1/events?resourceVersion=foo&resourceVersionMatch=Exact&limit=499", </del> <ins> name:       "request verb is list, resource version is zero, no limit", requestURI: "http://server/apis/foo.bar/v1/events?resourceVersion=0", </ins> requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar",

counts: map[string]int64{ "events.foo.bar": 799, }, <del> seatsExpected: 5, </del> <ins> seatsExpected: 8, }, { name:       "request verb is list, resource version match is Exact", requestURI: "http://server/apis/foo.bar/v1/events?resourceVersion=foo&resourceVersionMatch=Exact&limit=399", requestInfo: &apirequest.RequestInfo{ Verb:     "list", APIGroup: "foo.bar", Resource: "events", }, counts: map[string]int64{ "events.foo.bar": 699, }, seatsExpected: 8, </ins> }, { name:       "request verb is list, resource version match is NotOlderThan, limit not specified",