diff --git a/dovecot/keyproxy.go b/dovecot/keyproxy.go
index 61887b76eb45f0dbf331934eeaa8741efbff5274..c8b84f2d88cc902e520d5df0be02fcc261320f5d 100644
--- a/dovecot/keyproxy.go
+++ b/dovecot/keyproxy.go
@@ -10,6 +10,7 @@ import (
 
 	"git.autistici.org/ai3/go-common/clientutil"
 	"git.autistici.org/ai3/go-common/userenckey"
+	"golang.org/x/sync/singleflight"
 
 	"git.autistici.org/id/keystore/backend"
 	ldapBE "git.autistici.org/id/keystore/backend/ldap"
@@ -82,6 +83,7 @@ var passwordSep = "/"
 type KeyLookupProxy struct {
 	config   *Config
 	keystore client.Client
+	sfgroup  singleflight.Group
 	db       backend.Database
 }
 
@@ -151,7 +153,29 @@ func (s *KeyLookupProxy) lookupUserdb(ctx context.Context, username string) (int
 	return newUserDBResponse(s.b64encode(pub)), true, nil
 }
 
+type passdbLookupResult struct {
+	obj interface{}
+	ok  bool
+}
+
 func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool, error) {
+	// In order to wrap lookupPassdb() in a singleflight group we wrap its
+	// return parameters in a struct, with some additional error handling.
+	res, err, _ := s.sfgroup.Do(username, func() (interface{}, error) {
+		obj, ok, err := s.lookupPassdbReal(ctx, username, password)
+		if err != nil {
+			return nil, err
+		}
+		return &passdbLookupResult{obj: obj, ok: ok}, nil
+	})
+	if err != nil {
+		return nil, false, err
+	}
+	plr := res.(*passdbLookupResult)
+	return plr.obj, plr.ok, nil
+}
+
+func (s *KeyLookupProxy) lookupPassdbReal(ctx context.Context, username, password string) (interface{}, bool, error) {
 	// The password might be a SSO token, so first of all we try
 	// to fetch the unencrypted key from the keystore daemon.
 	var keystoreStatus string