diff --git a/server.go b/server.go
index 5c77d6607279841d8d1994231bf3165bb94086dd..79d25a48dcfcc70a77669d444a4eddbd5d22df98 100644
--- a/server.go
+++ b/server.go
@@ -15,6 +15,7 @@ import (
 	"io"
 	"log"
 	"path/filepath"
+	"sync"
 	"time"
 
 	"git.autistici.org/ai3/go-common/clientutil"
@@ -126,11 +127,19 @@ var (
 	errorRetryTimeout = 10 * time.Minute
 )
 
-func (m *Manager) updateAllCerts(ctx context.Context) {
-	for _, certInfo := range m.certs {
+func (m *Manager) updateAllCerts(ctx context.Context, certs []*certInfo) {
+	for _, certInfo := range certs {
 		if certInfo.retryDeadline.After(time.Now()) {
 			continue
 		}
+
+		// Abort the loop if our context is canceled.
+		select {
+		case <-ctx.Done():
+			return
+		default:
+		}
+
 		uctx, cancel := context.WithTimeout(ctx, renewalTimeout)
 		err := m.updateCert(uctx, certInfo)
 		cancel()
@@ -174,7 +183,7 @@ func (m *Manager) updateCert(ctx context.Context, certInfo *certInfo) error {
 }
 
 // Replace the current configuration.
-func (m *Manager) loadConfig(certDomains [][]string) {
+func (m *Manager) loadConfig(certDomains [][]string) []*certInfo {
 	var certs []*certInfo
 	for _, domains := range certDomains {
 		cn := domains[0]
@@ -197,7 +206,7 @@ func (m *Manager) loadConfig(certDomains [][]string) {
 		}
 		certs = append(certs, certInfo)
 	}
-	m.certs = certs
+	return certs
 }
 
 // This channel is used by the testing code to trigger an update,
@@ -205,15 +214,43 @@ func (m *Manager) loadConfig(certDomains [][]string) {
 var testUpdateCh = make(chan bool)
 
 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.
+	var upCancel context.CancelFunc
+	var wg sync.WaitGroup
+
+	startUpdate := func(certs []*certInfo) context.CancelFunc {
+		// Ensure the previous update has finished.
+		wg.Wait()
+
+		upCtx, cancel := context.WithCancel(ctx)
+		wg.Add(1)
+		go func() {
+			m.updateAllCerts(upCtx, certs)
+			wg.Done()
+		}()
+		return cancel
+	}
+
+	cancelUpdate := func() {
+		if upCancel != nil {
+			upCancel()
+		}
+		wg.Wait()
+	}
+	defer cancelUpdate()
+
 	tick := time.NewTicker(5 * time.Minute)
 	for {
 		select {
 		case <-tick.C:
-			m.updateAllCerts(ctx)
+			upCancel = startUpdate(m.certs)
 		case <-testUpdateCh:
-			m.updateAllCerts(ctx)
+			upCancel = startUpdate(m.certs)
 		case certDomains := <-m.configCh:
-			m.loadConfig(certDomains)
+			cancelUpdate()
+			m.certs = m.loadConfig(certDomains)
 		case <-m.stopCh:
 			return
 		case <-ctx.Done():