diff --git a/benchmarks/go.mod b/benchmarks/go.mod
index 8024e66b2..acd36a71d 100644
--- a/benchmarks/go.mod
+++ b/benchmarks/go.mod
@@ -16,7 +16,6 @@ require (
 )
 
 require (
-	github.com/benbjohnson/clock v1.3.0 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
diff --git a/benchmarks/go.sum b/benchmarks/go.sum
index 4ae6da835..c5ab9b30e 100644
--- a/benchmarks/go.sum
+++ b/benchmarks/go.sum
@@ -5,8 +5,6 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy
 github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
 github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
diff --git a/exp/go.sum b/exp/go.sum
index 96489348a..89b07e5ed 100644
--- a/exp/go.sum
+++ b/exp/go.sum
@@ -1,4 +1,3 @@
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/go.mod b/go.mod
index 455dae496..9a091d941 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module go.uber.org/zap
 go 1.19
 
 require (
-	github.com/benbjohnson/clock v1.3.0
 	github.com/stretchr/testify v1.8.1
 	go.uber.org/goleak v1.2.0
 	go.uber.org/multierr v1.10.0
diff --git a/go.sum b/go.sum
index ffa795531..6f3b5b06c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,3 @@
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
diff --git a/internal/ztest/clock.go b/internal/ztest/clock.go
index fe8026d94..cfdd83d69 100644
--- a/internal/ztest/clock.go
+++ b/internal/ztest/clock.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2021 Uber Technologies, Inc.
+// Copyright (c) 2023 Uber Technologies, Inc.
 //
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
@@ -21,30 +21,167 @@
 package ztest
 
 import (
+	"container/heap"
+	"sync"
 	"time"
-
-	"github.com/benbjohnson/clock"
 )
 
-// MockClock provides control over the time.
-type MockClock struct{ m *clock.Mock }
+// MockClock is a fake source of time.
+// It implements standard time operations,
+// but allows the user to control the passage of time.
+//
+// Use the [Add] method to progress time.
+type MockClock struct {
+	mu  sync.RWMutex
+	now time.Time
 
-// NewMockClock builds a new mock clock that provides control of time.
+	// The MockClock works by maintaining a list of waiters.
+	// Each waiter knows the time at which it should be resolved.
+	// When the clock advances, all waiters that are in range are resolved
+	// in chronological order.
+	waiters waiters
+}
+
+// NewMockClock builds a new mock clock
+// using the current actual time as the initial time.
 func NewMockClock() *MockClock {
-	return &MockClock{clock.NewMock()}
+	return &MockClock{
+		now: time.Now(),
+	}
 }
 
 // Now reports the current time.
 func (c *MockClock) Now() time.Time {
-	return c.m.Now()
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+	return c.now
 }
 
 // NewTicker returns a time.Ticker that ticks at the specified frequency.
+//
+// As with [time.NewTicker],
+// the ticker will drop ticks if the receiver is slow,
+// and the channel is never closed.
 func (c *MockClock) NewTicker(d time.Duration) *time.Ticker {
-	return &time.Ticker{C: c.m.Ticker(d).C}
+	ch := make(chan time.Time, 1)
+
+	var tick func(time.Time)
+	tick = func(now time.Time) {
+		next := now.Add(d)
+		c.runAt(next, func() {
+			defer tick(next)
+
+			select {
+			case ch <- next:
+				// ok
+			default:
+				// The receiver is slow.
+				// Drop the tick and continue.
+			}
+		})
+	}
+	tick(c.Now())
+
+	return &time.Ticker{C: ch}
 }
 
 // Add progresses time by the given duration.
+//
+// Other operations waiting for the time to advance
+// will be resolved if they are within range.
+//
+// Panics if the duration is negative.
 func (c *MockClock) Add(d time.Duration) {
-	c.m.Add(d)
+	if d < 0 {
+		panic("cannot add negative duration")
+	}
+
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	newTime := c.now.Add(d)
+	// newTime won't be recorded until the end of this method.
+	// This ensures that any waiters that are resolved
+	// are resolved at the time they were expecting.
+
+	for w, ok := c.waiters.PopLTE(newTime); ok; w, ok = c.waiters.PopLTE(newTime) {
+		// The waiter is within range.
+		// Travel to the time of the waiter and resolve it.
+		c.now = w.until
+
+		// The waiter may schedule more work
+		// so we must release the lock.
+		c.mu.Unlock()
+		w.fn()
+		// Sleeping here is necessary to let the side effects of waiters
+		// take effect before we continue.
+		time.Sleep(1 * time.Millisecond)
+		c.mu.Lock()
+	}
+
+	c.now = newTime
+}
+
+// runAt schedules the given function to be run at the given time.
+// The function runs without a lock held, so it may schedule more work.
+func (c *MockClock) runAt(t time.Time, fn func()) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	c.waiters.Push(waiter{until: t, fn: fn})
+}
+
+type waiter struct {
+	until time.Time
+	fn    func()
+}
+
+// waiters is a thread-safe collection of waiters
+// with the next waiter to be resolved at the front.
+//
+// Use the methods on this type to manipulate the collection.
+// Do not modify the slice directly.
+type waiters struct{ heap waiterHeap }
+
+// Push adds a new waiter to the collection.
+func (w *waiters) Push(v waiter) {
+	heap.Push(&w.heap, v)
+}
+
+// PopLTE removes and returns the next waiter to be resolved
+// if it is scheduled to be resolved at or before the given time.
+//
+// Returns false if there are no waiters in range.
+func (w *waiters) PopLTE(t time.Time) (_ waiter, ok bool) {
+	if len(w.heap) == 0 || w.heap[0].until.After(t) {
+		return waiter{}, false
+	}
+
+	return heap.Pop(&w.heap).(waiter), true
+}
+
+// waiterHeap implements a min-heap of waiters based on their 'until' time.
+//
+// This is separate from the waiters type so that we can implement heap.Interface
+// while still exposing a type-safe API on waiters.
+type waiterHeap []waiter
+
+var _ heap.Interface = (*waiterHeap)(nil)
+
+func (h waiterHeap) Len() int      { return len(h) }
+func (h waiterHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
+
+func (h waiterHeap) Less(i, j int) bool {
+	return h[i].until.Before(h[j].until)
+}
+
+func (h *waiterHeap) Push(x interface{}) {
+	*h = append(*h, x.(waiter))
+}
+
+func (h *waiterHeap) Pop() interface{} {
+	old := *h
+	n := len(old)
+	x := old[n-1]
+	*h = old[:n-1]
+	return x
 }
diff --git a/zapgrpc/internal/test/go.sum b/zapgrpc/internal/test/go.sum
index 51be79372..c2611afc8 100644
--- a/zapgrpc/internal/test/go.sum
+++ b/zapgrpc/internal/test/go.sum
@@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=