Repository: traefik/traefik (language: go)

Issue:
Increased memory usage and memory leak in Traefik v3
After upgrading to Traefik 3 (v3.0.3) I noticed it uses a lot more memory (`70 MB` vs `5 GB`) than v2 did. And it keeps increasing, indicating a possible memory leak.

![image](https://github.com/traefik/traefik/assets/1036439/c5973f42-5f97-4c68-a07b-5281a5c0285a)

Switching traefik container back to 2.11 fixes the issue.


I have 20 routers, 17 services and 12 middlewares (all HTTP). Most from docker container labels.

### What version of Traefik are you using?

3.0.3

### What is your environment & configuration?

traefik.yml
```yaml
metrics:
  prometheus:
    entryPoint: metrics
    addEntryPointsLabels: true
    addRoutersLabels: true
    addServicesLabels: true
    buckets:
      - 0.1
      - 0.3
      - 1.2
      - 5.0
global:
  checkNewVersion: true
  sendAnonymousUsage: false
log:
  filePath: /var/log/traefik/traefik.log
  format: json
accessLog:
  filePath: /var/log/traefik/access.log
api:
  dashboard: true
  insecure: false
certificatesResolvers:
  le:
    acme:
      email: ***
      dnsChallenge:
        provider: cloudflare
        delayBeforeCheck: 0
      storage: /acme/acme.json
entryPoints:
  metrics:
    address: :8899
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: :443
    http:
      tls:
        certResolver: le
    forwardedHeaders:
      trustedIPs:
        - ***
ping:
  entryPoint: websecure
providers:
  docker:
    exposedByDefault: false
    endpoint: "unix:///var/run/docker.sock"
  file:
    directory: /etc/traefik
    watch: true

experimental:
  plugins:
    real-ip:
      moduleName: github.com/Paxxs/traefik-get-real-ip
      version: v1.0.3

```

dynamic.yml
```yaml
http:
  middlewares:
    test1:
      chain:
        middlewares:
          - compression
          - real-ip-plugin

    test2:
      retry:
        attempts: 1
        initialinterval: 100ms

    redirect-to-www:
      redirectregex:
        regex: ^https://(?:www\.)?(.*)
        replacement: https://www.$${1}

    auth:
      basicAuth:
        users:
          - "***"

    compression:
      compress:
        excludedContentTypes:
          - text/event-stream

    security:
      headers:
        accessControlAllowOriginList:
          - "*"

    real-ip-plugin:
      plugin:
        real-ip:
          Proxy:
            - proxyHeadername: ***
              proxyHeadervalue: ***
              realIP: ***
              OverwriteXFF: true
            - proxyHeadername: "*"
              realIP: ***

  serversTransports:
    insecure:
      insecureSkipVerify: true
```
docker-compose.yml

```yaml
networks:
  traefik:
    name: traefik
    enable_ipv6: false
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.3.0/24
          gateway: 192.168.3.1

services:
  traefik:
    #image: traefik:v2.11
    image: traefik:latest
    container_name: traefik
    restart: unless-stopped
    networks:
      - traefik
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/:/etc/traefik/
      - ${DOCKER_DATA}/traefik/acme:/acme
      - ${DOCKER_DATA}/traefik/logs:/var/log/traefik
    environment:
      BASE_DOMAIN: ${BASE_DOMAIN}
      CF_ZONE_API_TOKEN: ${CF_ZONE_API_TOKEN}
      CLOUDFLARE_DNS_API_TOKEN: ${CF_ZONE_API_TOKEN}
    labels:
      - "traefik.docker.network=traefik"
      - "traefik.enable=true"
      - traefik.http.routers.traefik.rule=Host(`traefik.${BASE_DOMAIN}`)
      - traefik.http.routers.traefik.entrypoints=websecure
      - traefik.http.routers.traefik.service=api@internal
      
      # Forward auth
      - traefik.http.middlewares.auth-traefik.forwardauth.address=http://authentik-outpost:9000/outpost.goauthentik.io/auth/traefik
      - traefik.http.middlewares.auth-traefik.forwardauth.trustForwardHeader=true
      - traefik.http.routers.traefik.middlewares=security@file, test1@file, auth-traefik@docker
      # Route logout pages
      - traefik.http.routers.traefik-auth.middlewares=security@file, test1@file
      - traefik.http.routers.traefik-auth.rule=Host(`traefik.${BASE_DOMAIN}`) && PathPrefix(`/outpost.goauthentik.io/`)
      - traefik.http.routers.traefik-auth.service=authentik-proxy@docker
      - traefik.http.routers.traefik-auth.entrypoints=websecure
      - traefik.http.routers.traefik-auth.tls.domains[0].main=${BASE_DOMAIN}
      - traefik.http.routers.traefik-auth.tls.domains[0].sans=*.${BASE_DOMAIN}

      - com.centurylinklabs.watchtower.enable=true
      - com.centurylinklabs.watchtower.scope=daily
```



Discussion / hints:
I checked it on another server where v3.0.3 is running with almost the same configuration but even more routers and services, and it doesn't have that issue. Any recommendations on what to check?
Hello,

Could you share both configuration (static and dynamic) for the 2 use cases?
Could you try to enable the debug endpoints and to export pprof for heap (https://doc.traefik.io/traefik/operations/api/#debug)

enable debug endpoints by adding:
```
--api.debug
```

( if you are in an isolated environment, you should add --api.insecure to simplify the router configuration for this debug endpoints ) and then call

```
curl -o heap.pprof <traefik_ip>:<entrypoint_traefik_port>/debug/pprof/heap 
```

I've been experiencing the same issue after upgrading to v3. Memory usage would just keep increasing until an OOM event. I found that when i set `watch: false` on the file provider, however, the problem went away. I'm not sure why exactly, as I haven't had a chance to investigate further, but perhaps try setting that to false and see if it helps?
@joekrill does this setting work only if use config file?

I tried this:

![image](https://github.com/user-attachments/assets/572d22f9-562e-48ce-9817-f50c7a9bfc5e)

but got the error

`ERR Cannot start the provider *file.Provider error="error using file configuration provider, neither filename nor directory is defined"` ...
Forgot to mention it seems we are also hit with the memory issue ..
This is how it looks before upgrade/after upgrade to v3 (same command args for traefik):

![image](https://github.com/user-attachments/assets/dc3b6ebf-f47a-4466-b7d4-d5873f27ac1f)

> `ERR Cannot start the provider *file.Provider error="error using file configuration provider, neither filename nor directory is defined"` ...

@deyanp I'm not sure, to be honest. I would try disabling it using the config file, instead. You must also be using the file provider in the first place, so if you don't even have it configured at all this won't matter. 
@joekrill , I do not have the file provider configured at all, so I guess your workaround is not applicable to me. 
Wondering how to get back to the very stable performance of Traefik v2 ... 

P.S. We are using the KubernetesCRD provider (- --providers.kubernetescrd) ...
@deyanp @Janhouse I also had a memory leak with the v3 version, when I removed the compress middleware the memory leak disappeared. Maybe you can try it.
> @deyanp @Janhouse I also had a memory leak with the v3 version, when I removed the compress middleware the memory leak disappeared. Maybe you can try it.

I have the same behavior here in v3.1.0
I'm using traefik v3.1.2 and I'm experiencing the same issue with a high memory consumption. No issue in traefik v2.
> @deyanp @Janhouse I also had a memory leak with the v3 version, when I removed the compress middleware the memory leak disappeared. Maybe you can try it.

I downgraded to v2 since this already is a big issue and I can't just disable compression, but some other weird things were also happening with v3 and sadly I don't have that much time to hunt bugs. :slightly_smiling_face: 
Will check again once this gets fixed.
Seeing the same. Using Traefik 3.1.0.
Large spike in memory usage when compression is enabled.
Seeing same issue with Traefik 3.1.2. Seems related to the new zstd compression feature

Some local test showed high memory usage from the [klauspost/compress](https://git.hub.com/klauspost/compress) library. 

[Documentation](https://pkg.go.dev/github.com/klauspost/compress/zstd#readme-usage) seems to suggest avoid creating too many encoders and re-using them instead (see this [issue](https://github.com/klauspost/compress/issues/928))

```
Showing nodes accounting for 92.75MB, 81.54% of 113.74MB total
Dropped 140 nodes (cum <= 0.57MB)
Showing top 10 nodes out of 151
      flat  flat%   sum%        cum   cum%
      48MB 42.20% 42.20%       48MB 42.20%  github.com/klauspost/compress/zstd.(*fastBase).ensureHist
    9.58MB  8.42% 50.62%     9.58MB  8.42%  github.com/klauspost/compress/zstd.encoderOptions.encoder
```


Any ideas here? @juliens @nmengin 
@aas47 needs more time to analyze. 
However, providing a pprof when the issue occurs could definitely help speed things up.
hello, i have  a similar problem memory consumption on traefi v3 @Janhouse , i 've inspecting your traefik configuration and i use Authentik sso , maybe a lead regarding our concern?
![image](https://github.com/user-attachments/assets/37818bb6-228e-40f6-93ed-dc3e5f2de4c6)

I have the same problem, I don't know how to debug.

If a log is needed, I have prometheus and grafana collecting metrics
Thank you all for your input and for sharing your experiences regarding the memory leak. After investigating on my end, I haven't been able to reproduce the memory leak issue with any compression method, including br and zstd. While it's true that compression does use additional memory, in my tests, memory usage returns to normal levels once traffic decreases.

However, there is one notable difference I would like to highlight between Traefik v2 and v3 regarding compression. In v2, only gzip was supported, whereas in v3, we added support for Brotli (br) and Zstandard (zstd). These newer compression algorithms are more memory-intensive compared to gzip. From my tests, I've observed that gzip consumes around 200MB, whereas Brotli and Zstandard can go up to 900MB.

To address this, a PR has been merged that introduces the ability to select which compression algorithms you want to enable. This feature will be available in the next minor release, which is expected to be released very soon.

In the meantime, to further investigate this potential memory leak, could someone provide a complete reproducible case? This would greatly help us narrow down the issue. Ideally, here's what we would need:

 - Full Traefik configuration (including any middleware or specific settings related to compression).
 - Details about the backends you're using (e.g., type of services, behavior under load).
 - Traffic generation tool you're using to simulate load (e.g., curl, wrk, or other testing frameworks) and the exact parameters used during the tests.
 - If possible, any metrics or logs showing memory usage over time.

The more detailed the scenario, the quicker we can identify and resolve the issue.
Just want to add that I had almost no traffic during the time I experienced the memory leak.

On an off topic, I am happy to use v2 for now, since there were other major issues as well, like traefik v3 ignoring wildcard cert config and leaking all internal subdomains to transparency logs by requesting cert for each separate subdomain, instead of using/updating the wildcard one. 😡
Hello @Janhouse,

Thank you for your feedback.

> traefik v3 ignoring wildcard cert config and leaking all internal subdomains to transparency logs by requesting cert for each separate subdomain, instead of using/updating the wildcard one.

Could you elaborate on the issues you are facing, please?
> Thank you all for your input and for sharing your experiences regarding the memory leak. After investigating on my end, I haven't been able to reproduce the memory leak issue with any compression method, including br and zstd. While it's true that compression does use additional memory, in my tests, memory usage returns to normal levels once traffic decreases.
> 
> However, there is one notable difference I would like to highlight between Traefik v2 and v3 regarding compression. In v2, only gzip was supported, whereas in v3, we added support for Brotli (br) and Zstandard (zstd). These newer compression algorithms are more memory-intensive compared to gzip. From my tests, I've observed that gzip consumes around 200MB, whereas Brotli and Zstandard can go up to 900MB.
> 
> To address this, a PR has been merged that introduces the ability to select which compression algorithms you want to enable. This feature will be available in the next major release, which is expected to be released very soon.
> 
> In the meantime, to further investigate this potential memory leak, could someone provide a complete reproducible case? This would greatly help us narrow down the issue. Ideally, here's what we would need:
> 
> * Full Traefik configuration (including any middleware or specific settings related to compression).
> * Details about the backends you're using (e.g., type of services, behavior under load).
> * Traffic generation tool you're using to simulate load (e.g., curl, wrk, or other testing frameworks) and the exact parameters used during the tests.
> * If possible, any metrics or logs showing memory usage over time.
> 
> The more detailed the scenario, the quicker we can identify and resolve the issue.

Hello, it's currently my server configuration https://github.com/toutvukantabu/Homelab-traefik-authentik-crowdsec-server thanks a lot for your investigations! 
After some time of using v3 I noticed that my subdomains had appeared in transparency logs, after checking the acme file, I noticed it had acquired certs for each subdomain. I deleted them, started Traefik v3 again and they were requested again. Then I deleted the certs again, downgraded to Traefik v2 and it no longer reqested separate subdomain certs and used the wildcard one.
This was my 3rd or 4th big issue with v3 so I kind of gave up and did not spend more time debugging and reporting. I figured it was a beta quality release and I would not try v3 again for 6 to 12 months, or until those other issues are solved first. I really like Traefik and v2 is great but tests apparently are missing for a lot of scenarios.
Same problem here with memory usage, version 3.1.5 seems to fix the problem but it come back with 3.1.6 (despite the improvements reported by changelog).

![Image](https://github.com/user-attachments/assets/6c88af9c-cf86-43a4-9cde-e36616b2fc2c)

No plugin used but a few middlewares (compress, basicauth, ratelimit, stripprefix....)
Is this going to be finally fixed ? 3.1.7 (again?) has the problem?? Had to increase the memory limits yesterday for traefik pods to 500Mi due to hitting 300Mi and restarting a couple of times ... this has never been the case for traefik 2.x, it has always been very memory-efficient! 

No plugins, some middlewares, including compression ...

```
# https://doc.traefik.io/traefik/middlewares/http/compress/
# Enable gzip compression
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: compress-response
spec:
  compress: {}

```
After running some tests, I can confirm that the memory usage is linked to the compression algorithms.

Here are the results I observed:

Without compression: Memory usage is around 2-3 MB.
With Gzip enabled: Memory usage goes up to 20-30 MB.
With Zstandard (Zstd) enabled: Memory usage is between 60-80 MB.
With Brotli enabled: Memory usage increases to 350-500 MB.
The new compression methods introduced in Traefik 3.x seem to require more memory compared to Gzip or no compression. Starting from version 3.2, there is a new feature that allows excluding certain compressions, which can help reduce memory usage.
https://doc.traefik.io/traefik/v3.2/middlewares/http/compress/#encodings

I suggest testing with the 3.2 RC version, as the final release should arrive soon.
@juliens , I assume traefik 2.x supported only gzip, that is why memory footprint was low, right?
Thinking of enabling only gzip once 3.2 gets released ...
@deyanp 
Yes, brotli and zstd were introduce in 3.0. And yes I think the best solution to keep the same memory footprint will be to enable only gzip.
It still looks like a leak, not just a "larger memory footprint". When does it GC then? Never? How much memory has to be allocated?

Does enabling the different compression algos by default make any sense with the increased memory uage?
@Janhouse I wasn’t able to reproduce the leak, but I did reproduce the "larger memory footprint."

> Does enabling the different compression algos by default make any sense with the increased memory usage?

Personally, if we change this now, it might feel like a breaking change. Some users may have higher memory limits and prefer to use more memory with all the compression enabled.

For me, the main issue was that we couldn’t configure it, but this will be possible in v3.2.
I understand the reasoning, but if if I was a new Traefik user and it would use 5GB of RAM, I would no longer be a Traefik user.

V3 at the state of the 3.0 release would absolutely scare me away from it.
@Janhouse I understand your concern, but compression is not enabled by default. So, a new Traefik user won’t immediately use 5GB of RAM unless they enable the compression middleware. That said, this is just my personal opinion. I’ll share your feedback with the other maintainers during the next triage and get back to you as soon as possible.
@juliens I deployed traefik 3.1.6 this morning to my cluster, with compress middleware still configured for all ingressroutes, but the central config of it having a very high minResponseBodyBytes of 100000 effectively disabling the compression. Still, I observe high amounts (for our system) of memory used by the traefik pods of about 200-400Mi+ (after only a few hours) ...

Wondering if I have to really remove the compress middleware from all ingressroutes in addition, which would be a bummer ...

In v3.2.0, the problem has been optimized, but it is still not at the same level as v2.11.13.
Combined with the above problem analysis and solution, finally through the constraint of "preferential use of Gzip compression algorithm", the same result is obtained in v3.2.0 and v2.11.13. Thank you very much!
Under a simple 100 user locust stress test I notice my traefik RAM usage (in a docker-compose stack) shot up to about 1.4gb before I stopped it
`    image: traefik:3.2`

When I changed the docker-compose to
`    image: traefik:2.11` 
I hit about 140mb max

default settings everywhere, compression middleware enabled.

3.2 was choosing to use zstd 
2.11 is using gzip of course

I cannot upgrade to 3.X from 2.11 due to this issue. Definitely appeared like a memory leak would (3.2 memory kept growing and growing under stress test, likely due to zstd).

@mattbha With v3.2 we recommend to configure the encodings you need thanks to the [encodings option](https://doc.traefik.io/traefik/middlewares/http/compress/#encodings).
After testing v3.2 with gzip only, I confirm the issue is solved.
I think the default option (zstd) having this issue wouldn't let this be "solved". Probably should revert the default to gzip.

I'd prefer to use zstd where possible since it yields slightly better performance/compression.

While I agree manually becoming aware of this issue, and having the knowledge to switch back to gzip, does resolve the symptoms, I have to consider the rock solid stability of 2.11 -- I can't risk encountering further potential tripwires within 3.x
There is still a memory issue with the zstd compression, not sure this issue can really be marked as solved.
We disabled zstd compression in Traefik v3 after we run into memory leak issues. 

After upgrading from v2 to v3, memory usage was six times higher than before, so our pods were OOM killed. We gave them more memory and it was fully utilized. After switching back to gzip, everything was back to normal.
We also resolved it by switching the compression to gzip. As we have millions of requests per minute, our Traefik lasted for 2-3 minutes before hitting the 300 MB memory limit. 

Looks like you should include stress testing so you can catch things like these before switching something to default. And thanks for the solution.
Same here; I had the same issue after upgrade from 2.11 to 3.2, memory usage reached up to >5 GB, the nodes were acting crazy, how did they miss this issue with zstd !

I set this to fix it for now as I can't rollback to 2.11 especially after the CRD api changes.

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: compress
  namespace: ******
spec:
  compress:
    defaultEncoding: gzip
    encodings:
      - gzip
> After testing v3.2 with gzip only, I confirm the issue is solved.

this is not solved. as per the ongoing discussion
I find quite a few issues regarding zstd memory usage throughout different projects.
I.e. https://github.com/open-telemetry/opentelemetry-collector/pull/9749
Eventually someone can spot an issue or solution skimming over those.
Hey folk,

We re-open an issue to check if we can provide a better solution on such a topic.
Having same problem with brotli compression during user spikes/ddos.
It would be nice if there was a config option to limit the size of compression dictionary, so it doesn't cause OOM.
I'm encountering this issue, even with staying on `:v2` (incompatible syntax changes do not allow me to upgrade to v3 just yet)

Edit: actually, that may just be https://github.com/traefik/traefik/issues/11391
Hello @ShadowJonathan,

Thanks for your interest in Traefik!

> incompatible syntax changes do not allow me to upgrade to v3 just yet

There is a migration guide you can follow to define the default syntax format: https://doc.traefik.io/traefik/migration/v2-to-v3/

> Edit: actually, that may just be https://github.com/traefik/traefik/issues/11391

I can see that #11391 was probably more related to your issue, and I'm hiding your comment accordingly.
I encountered multiple OOMs with v3.1.2, but just updated it v3.3.4 - will provide more info if this doesn't help.
> Just want to add that I had almost no traffic during the time I experienced the memory leak.
> 
> On an off topic, I am happy to use v2 for now, since there were other major issues as well, like traefik v3 ignoring wildcard cert config and leaking all internal subdomains to transparency logs by requesting cert for each separate subdomain, instead of using/updating the wildcard one. 😡

I had a similar situation. There were hardly any requests to Traefik, but the memory kept increasing until it triggered an Out Of Memory (OOM). I released it approximately in November last year. By February, the first Pod experienced an OOM, and on March 14th, the second Pod triggered an OOM.

I've roughly identified the cause on my side: We carried out some internal renovations. As a result of external traffic refreshes, a large number of interfaces with different URLs returned a 404 status. The request URLs were included in the metric labels of Prometheus, and the dimension of the metrics was too large. There was a problem in the processing of a part of them, which led to the memory being filled up.  

Fix patch:
diff --git a/docs/content/middlewares/http/compress.md b/docs/content/middlewares/http/compress.md
index 1f204be296..58d06de969 100644
--- a/docs/content/middlewares/http/compress.md
+++ b/docs/content/middlewares/http/compress.md
@@ -264,10 +264,10 @@ http:
 
 ### `encodings`
 
-_Optional, Default="zstd, br, gzip"_
+_Optional, Default="gzip, br, zstd"_
 
 `encodings` specifies the list of supported compression encodings.
-At least one encoding value must be specified, and valid entries are `zstd` (Zstandard), `br` (Brotli), and `gzip` (Gzip).
+At least one encoding value must be specified, and valid entries are `gzip` (Gzip), `br` (Brotli), and `zstd` (Zstandard).
 The order of the list also sets the priority, the top entry has the highest priority.
 
 ```yaml tab="Docker & Swarm"
diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md
index 4f2f1d6e6a..fdbf5b5132 100644
--- a/docs/content/migration/v3.md
+++ b/docs/content/migration/v3.md
@@ -187,3 +187,13 @@ and will be removed in the next major version.
 
 In `v3.3.4`, the OpenTelemetry Request Duration metric (named `traefik_(entrypoint|router|service)_request_duration_seconds`) unit has been changed from milliseconds to seconds.
 To be consistent with the naming and other metrics providers, the metric now reports the duration in seconds.
+
+## v3.3.5
+
+### Compress Middleware
+
+In `v3.3.5`, the compress middleware `encodings` option default value is now `gzip, br, zstd`. 
+This change helps the algorithm selection to favor the `gzip` algorithm over the other algorithms.
+
+It impacts requests that do not specify their preferred algorithm,
+or has no order preference, in the `Accept-Encoding` header.
diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go
index 87a464e6cb..ea57f45183 100644
--- a/pkg/config/dynamic/middlewares.go
+++ b/pkg/config/dynamic/middlewares.go
@@ -187,7 +187,7 @@ type Compress struct {
 }
 
 func (c *Compress) SetDefaults() {
-	c.Encodings = []string{"zstd", "br", "gzip"}
+	c.Encodings = []string{"gzip", "br", "zstd"}
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/middlewares/compress/acceptencoding.go b/pkg/middlewares/compress/acceptencoding.go
index c09c84130f..a35362733f 100644
--- a/pkg/middlewares/compress/acceptencoding.go
+++ b/pkg/middlewares/compress/acceptencoding.go
@@ -23,57 +23,44 @@ type Encoding struct {
 	Weight float64
 }
 
-func getCompressionEncoding(acceptEncoding []string, defaultEncoding string, supportedEncodings []string) string {
-	if defaultEncoding == "" {
-		if slices.Contains(supportedEncodings, brotliName) {
-			// Keeps the pre-existing default inside Traefik if brotli is a supported encoding.
-			defaultEncoding = brotliName
-		} else if len(supportedEncodings) > 0 {
-			// Otherwise use the first supported encoding.
-			defaultEncoding = supportedEncodings[0]
-		}
+func (c *compress) getCompressionEncoding(acceptEncoding []string) string {
+	// RFC says: An Accept-Encoding header field with a field value that is empty implies that the user agent does not want any content coding in response.
+	// https://datatracker.ietf.org/doc/html/rfc9110#name-accept-encoding
+	if len(acceptEncoding) == 1 && acceptEncoding[0] == "" {
+		return identityName
 	}
 
-	encodings, hasWeight := parseAcceptEncoding(acceptEncoding, supportedEncodings)
-
-	if hasWeight {
-		if len(encodings) == 0 {
-			return identityName
-		}
+	acceptableEncodings := parseAcceptableEncodings(acceptEncoding, c.supportedEncodings)
 
-		encoding := encodings[0]
-
-		if encoding.Type == identityName && encoding.Weight == 0 {
-			return notAcceptable
-		}
-
-		if encoding.Type == wildcardName && encoding.Weight == 0 {
-			return notAcceptable
-		}
+	// An empty Accept-Encoding header field would have been handled earlier.
+	// If empty, it means no encoding is supported, we do not encode.
+	if len(acceptableEncodings) == 0 {
+		// TODO: return 415 status code instead of deactivating the compression, if the backend was not to compress as well.
+		return notAcceptable
+	}
 
-		if encoding.Type == wildcardName {
-			return defaultEncoding
+	slices.SortFunc(acceptableEncodings, func(a, b Encoding) int {
+		if a.Weight == b.Weight {
+			// At same weight, we want to prioritize based on the encoding priority.
+			// the lower the index, the higher the priority.
+			return cmp.Compare(c.supportedEncodings[a.Type], c.supportedEncodings[b.Type])
 		}
+		return cmp.Compare(b.Weight, a.Weight)
+	})
 
-		return encoding.Type
-	}
-
-	for _, dt := range supportedEncodings {
-		if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == dt }) {
-			return dt
+	if acceptableEncodings[0].Type == wildcardName {
+		if c.defaultEncoding == "" {
+			return c.encodings[0]
 		}
-	}
 
-	if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == wildcardName }) {
-		return defaultEncoding
+		return c.defaultEncoding
 	}
 
-	return identityName
+	return acceptableEncodings[0].Type
 }
 
-func parseAcceptEncoding(acceptEncoding, supportedEncodings []string) ([]Encoding, bool) {
+func parseAcceptableEncodings(acceptEncoding []string, supportedEncodings map[string]int) []Encoding {
 	var encodings []Encoding
-	var hasWeight bool
 
 	for _, line := range acceptEncoding {
 		for _, item := range strings.Split(strings.ReplaceAll(line, " ", ""), ",") {
@@ -82,9 +69,7 @@ func parseAcceptEncoding(acceptEncoding, supportedEncodings []string) ([]Encodin
 				continue
 			}
 
-			if !slices.Contains(supportedEncodings, parsed[0]) &&
-				parsed[0] != identityName &&
-				parsed[0] != wildcardName {
+			if _, ok := supportedEncodings[parsed[0]]; !ok {
 				continue
 			}
 
@@ -94,8 +79,13 @@ func parseAcceptEncoding(acceptEncoding, supportedEncodings []string) ([]Encodin
 			if len(parsed) > 1 && strings.HasPrefix(parsed[1], "q=") {
 				w, _ := strconv.ParseFloat(strings.TrimPrefix(parsed[1], "q="), 64)
 
+				// If the weight is 0, the encoding is not acceptable.
+				// We can skip the encoding.
+				if w == 0 {
+					continue
+				}
+
 				weight = w
-				hasWeight = true
 			}
 
 			encodings = append(encodings, Encoding{
@@ -105,9 +95,5 @@ func parseAcceptEncoding(acceptEncoding, supportedEncodings []string) ([]Encodin
 		}
 	}
 
-	slices.SortFunc(encodings, func(a, b Encoding) int {
-		return cmp.Compare(b.Weight, a.Weight)
-	})
-
-	return encodings, hasWeight
+	return encodings
 }
diff --git a/pkg/middlewares/compress/compress.go b/pkg/middlewares/compress/compress.go
index cdf4d972b7..0bb3788d2b 100644
--- a/pkg/middlewares/compress/compress.go
+++ b/pkg/middlewares/compress/compress.go
@@ -22,7 +22,7 @@ const typeName = "Compress"
 // See https://github.com/klauspost/compress/blob/9559b037e79ad673c71f6ef7c732c00949014cd2/gzhttp/compress.go#L47.
 const defaultMinSize = 1024
 
-var defaultSupportedEncodings = []string{zstdName, brotliName, gzipName}
+var defaultSupportedEncodings = []string{gzipName, brotliName, zstdName}
 
 // Compress is a middleware that allows to compress the response.
 type compress struct {
@@ -33,6 +33,8 @@ type compress struct {
 	minSize         int
 	encodings       []string
 	defaultEncoding string
+	// supportedEncodings is a map of supported encodings and their priority.
+	supportedEncodings map[string]int
 
 	brotliHandler http.Handler
 	gzipHandler   http.Handler
@@ -85,13 +87,14 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str
 	}
 
 	c := &compress{
-		next:            next,
-		name:            name,
-		excludes:        excludes,
-		includes:        includes,
-		minSize:         minSize,
-		encodings:       conf.Encodings,
-		defaultEncoding: conf.DefaultEncoding,
+		next:               next,
+		name:               name,
+		excludes:           excludes,
+		includes:           includes,
+		minSize:            minSize,
+		encodings:          conf.Encodings,
+		defaultEncoding:    conf.DefaultEncoding,
+		supportedEncodings: buildSupportedEncodings(conf.Encodings),
 	}
 
 	var err error
@@ -114,6 +117,19 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str
 	return c, nil
 }
 
+func buildSupportedEncodings(encodings []string) map[string]int {
+	supportedEncodings := map[string]int{
+		// the most permissive first.
+		wildcardName: -1,
+		// the less permissive last.
+		identityName: len(encodings),
+	}
+	for i, encoding := range encodings {
+		supportedEncodings[encoding] = i
+	}
+	return supportedEncodings
+}
+
 func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	logger := middlewares.GetLogger(req.Context(), c.name, typeName)
 
@@ -149,7 +165,7 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 		return
 	}
 
-	c.chooseHandler(getCompressionEncoding(acceptEncoding, c.defaultEncoding, c.encodings), rw, req)
+	c.chooseHandler(c.getCompressionEncoding(acceptEncoding), rw, req)
 }
 
 func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.Request) {


Test patch:
diff --git a/pkg/middlewares/compress/acceptencoding_test.go b/pkg/middlewares/compress/acceptencoding_test.go
index 570315b473..d3059af6e7 100644
--- a/pkg/middlewares/compress/acceptencoding_test.go
+++ b/pkg/middlewares/compress/acceptencoding_test.go
@@ -1,9 +1,12 @@
 package compress
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/traefik/traefik/v3/pkg/config/dynamic"
 )
 
 func Test_getCompressionEncoding(t *testing.T) {
@@ -15,14 +18,19 @@ func Test_getCompressionEncoding(t *testing.T) {
 		expected           string
 	}{
 		{
-			desc:           "br > gzip (no weight)",
+			desc:           "Empty Accept-Encoding",
+			acceptEncoding: []string{""},
+			expected:       identityName,
+		},
+		{
+			desc:           "gzip > br (no weight)",
 			acceptEncoding: []string{"gzip, br"},
-			expected:       brotliName,
+			expected:       gzipName,
 		},
 		{
-			desc:           "zstd > br > gzip (no weight)",
-			acceptEncoding: []string{"zstd, gzip, br"},
-			expected:       zstdName,
+			desc:           "gzip > br > zstd (no weight)",
+			acceptEncoding: []string{"gzip, br, zstd"},
+			expected:       gzipName,
 		},
 		{
 			desc:           "known compression encoding (no weight)",
@@ -32,24 +40,34 @@ func Test_getCompressionEncoding(t *testing.T) {
 		{
 			desc:           "unknown compression encoding (no weight), no encoding",
 			acceptEncoding: []string{"compress, rar"},
-			expected:       identityName,
+			expected:       notAcceptable,
 		},
 		{
-			desc:           "wildcard return the default compression encoding",
+			desc:           "wildcard returns the default compression encoding",
 			acceptEncoding: []string{"*"},
-			expected:       brotliName,
+			expected:       gzipName,
 		},
 		{
-			desc:            "wildcard return the custom default compression encoding",
+			desc:            "wildcard returns the custom default compression encoding",
 			acceptEncoding:  []string{"*"},
-			defaultEncoding: "foo",
-			expected:        "foo",
+			defaultEncoding: brotliName,
+			expected:        brotliName,
 		},
 		{
 			desc:           "follows weight",
 			acceptEncoding: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
 			expected:       gzipName,
 		},
+		{
+			desc:           "identity with higher weight is preferred",
+			acceptEncoding: []string{"br;q=0.8, identity;q=1.0"},
+			expected:       identityName,
+		},
+		{
+			desc:           "identity with equal weight is not preferred",
+			acceptEncoding: []string{"br;q=0.8, identity;q=0.8"},
+			expected:       brotliName,
+		},
 		{
 			desc:           "ignore unknown compression encoding",
 			acceptEncoding: []string{"compress;q=1.0, gzip;q=0.5"},
@@ -93,6 +111,33 @@ func Test_getCompressionEncoding(t *testing.T) {
 			supportedEncodings: []string{gzipName, brotliName},
 			expected:           gzipName,
 		},
+		{
+			desc:           "Zero weights, no compression",
+			acceptEncoding: []string{"br;q=0, gzip;q=0, zstd;q=0"},
+			expected:       notAcceptable,
+		},
+		{
+			desc:            "Zero weights, default encoding, no compression",
+			acceptEncoding:  []string{"br;q=0, gzip;q=0, zstd;q=0"},
+			defaultEncoding: "br",
+			expected:        notAcceptable,
+		},
+		{
+			desc:           "Same weight, first supported encoding",
+			acceptEncoding: []string{"br;q=1.0, gzip;q=1.0, zstd;q=1.0"},
+			expected:       gzipName,
+		},
+		{
+			desc:           "Same weight, first supported encoding, order has no effect",
+			acceptEncoding: []string{"br;q=1.0, zstd;q=1.0, gzip;q=1.0"},
+			expected:       gzipName,
+		},
+		{
+			desc:            "Same weight, first supported encoding, defaultEncoding has no effect",
+			acceptEncoding:  []string{"br;q=1.0, zstd;q=1.0, gzip;q=1.0"},
+			defaultEncoding: "br",
+			expected:        gzipName,
+		},
 	}
 
 	for _, test := range testCases {
@@ -103,7 +148,18 @@ func Test_getCompressionEncoding(t *testing.T) {
 				test.supportedEncodings = defaultSupportedEncodings
 			}
 
-			encoding := getCompressionEncoding(test.acceptEncoding, test.defaultEncoding, test.supportedEncodings)
+			conf := dynamic.Compress{
+				Encodings:       test.supportedEncodings,
+				DefaultEncoding: test.defaultEncoding,
+			}
+
+			h, err := New(context.Background(), nil, conf, "test")
+			require.NoError(t, err)
+
+			c, ok := h.(*compress)
+			require.True(t, ok)
+
+			encoding := c.getCompressionEncoding(test.acceptEncoding)
 
 			assert.Equal(t, test.expected, encoding)
 		})
@@ -147,7 +203,6 @@ func Test_parseAcceptEncoding(t *testing.T) {
 				{Type: zstdName, Weight: 1},
 				{Type: gzipName, Weight: 1},
 				{Type: brotliName, Weight: 1},
-				{Type: wildcardName, Weight: 0},
 			},
 			assertWeight: assert.True,
 		},
@@ -157,7 +212,6 @@ func Test_parseAcceptEncoding(t *testing.T) {
 			supportedEncodings: []string{zstdName},
 			expected: []Encoding{
 				{Type: zstdName, Weight: 1},
-				{Type: wildcardName, Weight: 0},
 			},
 			assertWeight: assert.True,
 		},
@@ -188,7 +242,6 @@ func Test_parseAcceptEncoding(t *testing.T) {
 			expected: []Encoding{
 				{Type: gzipName, Weight: 1},
 				{Type: identityName, Weight: 0.5},
-				{Type: wildcardName, Weight: 0},
 			},
 			assertWeight: assert.True,
 		},
@@ -198,7 +251,6 @@ func Test_parseAcceptEncoding(t *testing.T) {
 			supportedEncodings: []string{"br"},
 			expected: []Encoding{
 				{Type: identityName, Weight: 0.5},
-				{Type: wildcardName, Weight: 0},
 			},
 			assertWeight: assert.True,
 		},
@@ -212,10 +264,11 @@ func Test_parseAcceptEncoding(t *testing.T) {
 				test.supportedEncodings = defaultSupportedEncodings
 			}
 
-			aes, hasWeight := parseAcceptEncoding(test.values, test.supportedEncodings)
+			supportedEncodings := buildSupportedEncodings(test.supportedEncodings)
+
+			aes := parseAcceptableEncodings(test.values, supportedEncodings)
 
 			assert.Equal(t, test.expected, aes)
-			test.assertWeight(t, hasWeight)
 		})
 	}
 }
diff --git a/pkg/middlewares/compress/compress_test.go b/pkg/middlewares/compress/compress_test.go
index 430df76119..622471f2e1 100644
--- a/pkg/middlewares/compress/compress_test.go
+++ b/pkg/middlewares/compress/compress_test.go
@@ -39,9 +39,14 @@ func TestNegotiation(t *testing.T) {
 			expEncoding:     "",
 		},
 		{
+			// In this test, the default encodings are defaulted to gzip, brotli, and zstd,
+			// which make gzip the default encoding, and will be selected.
+			// However, the klauspost/compress gzhttp handler does not compress when Accept-Encoding: * is set.
+			// Until klauspost/compress gzhttp package supports the asterisk,
+			// we will not support it when selecting the gzip encoding.
 			desc:            "accept any header",
 			acceptEncHeader: "*",
-			expEncoding:     brotliName,
+			expEncoding:     "",
 		},
 		{
 			desc:            "gzip accept header",
@@ -66,7 +71,7 @@ func TestNegotiation(t *testing.T) {
 		{
 			desc:            "multi accept header list, prefer br",
 			acceptEncHeader: "gzip, br",
-			expEncoding:     brotliName,
+			expEncoding:     gzipName,
 		},
 		{
 			desc:            "zstd accept header",
@@ -78,15 +83,20 @@ func TestNegotiation(t *testing.T) {
 			acceptEncHeader: "zstd;q=0.9, br;q=0.8, gzip;q=0.6",
 			expEncoding:     zstdName,
 		},
+		{
+			desc:            "multi accept header, prefer brotli",
+			acceptEncHeader: "gzip;q=0.8, br;q=1.0, zstd;q=0.7",
+			expEncoding:     brotliName,
+		},
 		{
 			desc:            "multi accept header, prefer gzip",
 			acceptEncHeader: "gzip;q=1.0, br;q=0.8, zstd;q=0.7",
 			expEncoding:     gzipName,
 		},
 		{
-			desc:            "multi accept header list, prefer zstd",
+			desc:            "multi accept header list, prefer gzip",
 			acceptEncHeader: "gzip, br, zstd",
-			expEncoding:     zstdName,
+			expEncoding:     gzipName,
 		},
 	}
 
@@ -190,6 +200,28 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
 	assert.EqualValues(t, rw.Body.Bytes(), fakeBody)
 }
 
+func TestEmptyAcceptEncoding(t *testing.T) {
+	req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
+	req.Header.Add(acceptEncodingHeader, "")
+
+	fakeBody := generateBytes(gzhttp.DefaultMinSize)
+	next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		_, err := rw.Write(fakeBody)
+		if err != nil {
+			http.Error(rw, err.Error(), http.StatusInternalServerError)
+		}
+	})
+	handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
+	require.NoError(t, err)
+
+	rw := httptest.NewRecorder()
+	handler.ServeHTTP(rw, req)
+
+	assert.Empty(t, rw.Header().Get(contentEncodingHeader))
+	assert.Empty(t, rw.Header().Get(varyHeader))
+	assert.EqualValues(t, rw.Body.Bytes(), fakeBody)
+}
+
 func TestShouldNotCompressWhenIdentityAcceptEncodingHeader(t *testing.T) {
 	req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
 	req.Header.Set(acceptEncodingHeader, "identity")