diff --git a/manager.go b/manager.go
index cb8195c53c1f401c724af2e0c9da28c1634b3b02..06214358e76b62f490a94d4540452cc274ac39f9 100644
--- a/manager.go
+++ b/manager.go
@@ -249,12 +249,23 @@ func (m *Manager) loop(ctx context.Context) {
 	// Updates are long-term jobs, so they should be
 	// interruptible. We run updates in a separate goroutine, and
 	// cancel them when the configuration is reloaded or on exit.
+	// A simple channel is used as a semaphore, so that only one
+	// update goroutine can be running at any given time (without
+	// other ones piling up).
 	var upCancel context.CancelFunc
 	var wg sync.WaitGroup
+	sem := make(chan struct{}, 1)
 
 	startUpdate := func(certs []*certInfo) context.CancelFunc {
-		// Ensure the previous update has finished.
-		wg.Wait()
+		// Acquire the semaphore, return if we fail to.
+		select {
+		case sem <- struct{}{}:
+		default:
+			return nil
+		}
+		defer func() {
+			<-sem
+		}()
 
 		upCtx, cancel := context.WithCancel(ctx)
 		wg.Add(1)
@@ -270,6 +281,7 @@ func (m *Manager) loop(ctx context.Context) {
 	cancelUpdate := func() {
 		if upCancel != nil {
 			upCancel()
+			upCancel = nil
 		}
 		wg.Wait()
 	}
@@ -278,17 +290,21 @@ func (m *Manager) loop(ctx context.Context) {
 	tick := time.NewTicker(5 * time.Minute)
 	defer tick.Stop()
 	for {
+		var c func()
 		select {
 		case <-tick.C:
-			upCancel = startUpdate(m.certs)
+			c = startUpdate(m.certs)
 		case <-testUpdateCh:
-			upCancel = startUpdate(m.certs)
+			c = startUpdate(m.certs)
 		case certDomains := <-m.configCh:
 			cancelUpdate()
 			m.certs = m.loadConfig(certDomains)
 		case <-ctx.Done():
 			return
 		}
+		if c != nil {
+			upCancel = nil
+		}
 	}
 }