diff --git a/eth/filters/api.go b/eth/filters/api.go
index 41e15e0ddc..93071111a6 100644
--- a/eth/filters/api.go
+++ b/eth/filters/api.go
@@ -64,6 +64,8 @@ type FilterAPI struct {
filtersMu sync.Mutex
filters map[rpc.ID]*filter
timeout time.Duration
+
+ quit chan struct{}
}
// NewFilterAPI returns a new FilterAPI instance.
@@ -73,6 +75,7 @@ func NewFilterAPI(system *FilterSystem, lightMode bool) *FilterAPI {
events: NewEventSystem(system, lightMode),
filters: make(map[rpc.ID]*filter),
timeout: system.cfg.Timeout,
+ quit: make(chan struct{}),
}
go api.timeoutLoop(system.cfg.Timeout)
@@ -86,7 +89,11 @@ func (api *FilterAPI) timeoutLoop(timeout time.Duration) {
ticker := time.NewTicker(timeout)
defer ticker.Stop()
for {
- <-ticker.C
+ select {
+ case <-ticker.C:
+ case <-api.quit:
+ return
+ }
api.filtersMu.Lock()
for id, f := range api.filters {
select {
diff --git a/eth/filters/api.libevm.go b/eth/filters/api.libevm.go
new file mode 100644
index 0000000000..dff4f52ea6
--- /dev/null
+++ b/eth/filters/api.libevm.go
@@ -0,0 +1,22 @@
+// Copyright 2026 the libevm authors.
+//
+// The libevm additions to go-ethereum are free software: you can redistribute
+// them and/or modify them under the terms of the GNU Lesser General Public License
+// as published by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// The libevm additions are distributed in the hope that they will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+// General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see
+// .
+
+package filters
+
+// Close releases resources held by the API.
+func (api *FilterAPI) Close() {
+ close(api.quit)
+}
diff --git a/eth/filters/api.libevm_test.go b/eth/filters/api.libevm_test.go
new file mode 100644
index 0000000000..c18134ac01
--- /dev/null
+++ b/eth/filters/api.libevm_test.go
@@ -0,0 +1,57 @@
+// Copyright 2026 the libevm authors.
+//
+// The libevm additions to go-ethereum are free software: you can redistribute
+// them and/or modify them under the terms of the GNU Lesser General Public License
+// as published by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// The libevm additions are distributed in the hope that they will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+// General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see
+// .
+
+package filters
+
+import (
+ "testing"
+
+ "go.uber.org/goleak"
+
+ "github.com/ava-labs/libevm/core"
+ "github.com/ava-labs/libevm/core/rawdb"
+ "github.com/ava-labs/libevm/event"
+)
+
+// A closeableTestBackend tracks all subscriptions that it produces, allowing
+// them to be cleaned up to avoid the leak of [EventSystem.eventLoop].
+type closeableTestBackend struct {
+ testBackend
+ subs event.SubscriptionScope
+}
+
+func (b *closeableTestBackend) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription {
+ sub := b.testBackend.SubscribeNewTxsEvent(ch)
+ return b.subs.Track(sub)
+}
+
+func (b *closeableTestBackend) Close() {
+ b.subs.Close()
+}
+
+func TestClose(t *testing.T) {
+ defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
+
+ backend := &closeableTestBackend{
+ testBackend: testBackend{
+ db: rawdb.NewMemoryDatabase(),
+ },
+ }
+ defer backend.Close()
+ sys := NewFilterSystem(backend, Config{})
+ api := NewFilterAPI(sys, false)
+ api.Close()
+}
diff --git a/go.mod b/go.mod
index f966a6849f..0ae1145599 100644
--- a/go.mod
+++ b/go.mod
@@ -64,6 +64,7 @@ require (
github.com/tyler-smith/go-bip39 v1.1.0
github.com/urfave/cli/v2 v2.25.7
go.uber.org/automaxprocs v1.5.2
+ go.uber.org/goleak v1.3.0
golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
golang.org/x/mod v0.29.0
diff --git a/go.sum b/go.sum
index 53786439a4..3f876903d9 100644
--- a/go.sum
+++ b/go.sum
@@ -622,6 +622,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=