diff --git a/backend/ldap.go b/backend/ldap.go
index b24729cc6cea15e732485bd84b43a5d99e5b6930..03c2b755ea19ac057ea565231a2b3ac9401d7f91 100644
--- a/backend/ldap.go
+++ b/backend/ldap.go
@@ -6,8 +6,8 @@ import (
 	"io/ioutil"
 	"log"
 	"strings"
-	"time"
 
+	ldaputil "git.autistici.org/ai3/go-common/ldap"
 	"gopkg.in/ldap.v2"
 )
 
@@ -90,7 +90,7 @@ func (c *LDAPConfig) Valid() error {
 
 type ldapBackend struct {
 	config *LDAPConfig
-	conn   *ldap.Conn
+	pool   *ldaputil.ConnectionPool
 }
 
 func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) {
@@ -106,28 +106,19 @@ func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) {
 	}
 
 	// Connect.
-	conn, err := ldap.Dial("unix", "/var/lib/ldapi")
+	pool, err := ldaputil.NewConnectionPool(config.URI, config.BindDN, strings.TrimSpace(string(bindPw)), 5)
 	if err != nil {
 		return nil, err
 	}
-	if err = conn.Bind(config.BindDN, strings.TrimSpace(string(bindPw))); err != nil {
-		conn.Close()
-		return nil, err
-	}
 
 	return &ldapBackend{
 		config: config,
-		conn:   conn,
+		pool:   pool,
 	}, nil
 }
 
 func (b *ldapBackend) GetKeys(ctx context.Context, username string) [][]byte {
-	// Try to turn the context deadline into a LDAP connection timeout...
-	if deadline, ok := ctx.Deadline(); ok {
-		b.conn.SetTimeout(time.Until(deadline))
-	}
-
-	result, err := b.conn.Search(b.config.Query.searchRequest(username))
+	result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username))
 	if err != nil {
 		log.Printf("LDAP error: %v", err)
 		return nil
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/pool.go b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
new file mode 100644
index 0000000000000000000000000000000000000000..6d8093e93dccd5d333133633ab8958092a171355
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
@@ -0,0 +1,125 @@
+package ldaputil
+
+import (
+	"context"
+	"errors"
+	"net"
+	"net/url"
+	"time"
+
+	"gopkg.in/ldap.v2"
+)
+
+// ConnectionPool provides a goroutine-safe pool of long-lived LDAP
+// connections that will reconnect on errors.
+type ConnectionPool struct {
+	network string
+	addr    string
+	bindDN  string
+	bindPw  string
+
+	c chan *ldap.Conn
+}
+
+var defaultConnectTimeout = 5 * time.Second
+
+func (p *ConnectionPool) connect(ctx context.Context) (*ldap.Conn, error) {
+	// Dial the connection with a timeout, if the context has a
+	// deadline (as it should). If the context does not have a
+	// deadline, we set a default timeout.
+	deadline, ok := ctx.Deadline()
+	if !ok {
+		deadline = time.Now().Add(defaultConnectTimeout)
+	}
+
+	c, err := net.DialTimeout(p.network, p.addr, time.Until(deadline))
+	if err != nil {
+		return nil, err
+	}
+
+	conn := ldap.NewConn(c, false)
+	conn.Start()
+
+	conn.SetTimeout(time.Until(deadline))
+	if _, err = conn.SimpleBind(ldap.NewSimpleBindRequest(p.bindDN, p.bindPw, nil)); err != nil {
+		conn.Close()
+		return nil, err
+	}
+
+	return conn, err
+}
+
+// Get a fresh connection from the pool.
+func (p *ConnectionPool) Get(ctx context.Context) (*ldap.Conn, error) {
+	// Grab a connection from the cache, or create a new one if
+	// there are no available connections.
+	select {
+	case conn := <-p.c:
+		return conn, nil
+	default:
+		return p.connect(ctx)
+	}
+}
+
+// Release a used connection onto the pool.
+func (p *ConnectionPool) Release(conn *ldap.Conn, err error) {
+	// Connections that failed should not be reused.
+	if err != nil {
+		conn.Close()
+		return
+	}
+
+	// Return the connection to the cache, or close it if it's
+	// full.
+	select {
+	case p.c <- conn:
+	default:
+		conn.Close()
+	}
+}
+
+// Close all connections. Not implemented yet.
+func (p *ConnectionPool) Close() {}
+
+// Parse a LDAP URI into network and address strings suitable for
+// ldap.Dial.
+func parseLDAPURI(uri string) (string, string, error) {
+	u, err := url.Parse(uri)
+	if err != nil {
+		return "", "", err
+	}
+
+	network := "tcp"
+	addr := "localhost:389"
+	switch u.Scheme {
+	case "ldap":
+		if u.Host != "" {
+			addr = u.Host
+		}
+	case "ldapi":
+		network = "unix"
+		addr = u.Path
+	default:
+		return "", "", errors.New("unsupported scheme")
+	}
+
+	return network, addr, nil
+}
+
+// NewConnectionPool creates a new pool of LDAP connections to the
+// specified server, using the provided bind credentials. The pool
+// will cache at most cacheSize connections.
+func NewConnectionPool(uri, bindDN, bindPw string, cacheSize int) (*ConnectionPool, error) {
+	network, addr, err := parseLDAPURI(uri)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ConnectionPool{
+		c:       make(chan *ldap.Conn, cacheSize),
+		network: network,
+		addr:    addr,
+		bindDN:  bindDN,
+		bindPw:  bindPw,
+	}, nil
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/search.go b/vendor/git.autistici.org/ai3/go-common/ldap/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..872f6fec3fdb2a8516f7e3da55dddeb972b358f5
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/search.go
@@ -0,0 +1,54 @@
+package ldaputil
+
+import (
+	"context"
+	"time"
+
+	"github.com/cenkalti/backoff"
+	"gopkg.in/ldap.v2"
+
+	"git.autistici.org/ai3/go-common/clientutil"
+)
+
+// Treat all errors as potential network-level issues, except for a
+// whitelist of LDAP protocol level errors that we know are benign.
+func isTemporaryLDAPError(err error) bool {
+	ldapErr, ok := err.(*ldap.Error)
+	if !ok {
+		return true
+	}
+	switch ldapErr.ResultCode {
+	case ldap.ErrorNetwork:
+		return true
+	default:
+		return false
+	}
+}
+
+// Search performs the given search request. It will retry the request
+// on temporary errors.
+func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
+	var result *ldap.SearchResult
+	err := clientutil.Retry(func() error {
+		conn, err := p.Get(ctx)
+		if err != nil {
+			if isTemporaryLDAPError(err) {
+				return clientutil.TempError(err)
+			}
+			return err
+		}
+
+		if deadline, ok := ctx.Deadline(); ok {
+			conn.SetTimeout(time.Until(deadline))
+		}
+
+		result, err = conn.Search(searchRequest)
+		if err != nil && isTemporaryLDAPError(err) {
+			p.Release(conn, nil)
+			return clientutil.TempError(err)
+		}
+		p.Release(conn, err)
+		return err
+	}, backoff.WithContext(clientutil.NewExponentialBackOff(), ctx))
+	return result, err
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 15bcb981fe528f040dd24e5adb93e3122c839b9c..8026fa82d78bb30fe6ee5825cc4dcf6c801ea334 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -14,6 +14,12 @@
 			"revision": "c2c933578837d28b6f0e9b0b4b183d53ab28785e",
 			"revisionTime": "2017-12-11T08:01:45Z"
 		},
+		{
+			"checksumSHA1": "mEnXMNziH82HFtGngHU19VHTVHs=",
+			"path": "git.autistici.org/ai3/go-common/ldap",
+			"revision": "c2c933578837d28b6f0e9b0b4b183d53ab28785e",
+			"revisionTime": "2017-12-11T08:01:45Z"
+		},
 		{
 			"checksumSHA1": "3bComZxAfgnoTG4UDlyFgLyeykc=",
 			"path": "git.autistici.org/ai3/go-common/serverutil",
@@ -23,8 +29,8 @@
 		{
 			"checksumSHA1": "DFjm2ZJpUwioPApa3htGXLEFWl8=",
 			"path": "git.autistici.org/id/go-sso",
-			"revision": "8396b6ffcb3731465f1f0d05e2af4f5b139591b1",
-			"revisionTime": "2017-12-09T17:37:13Z"
+			"revision": "68704340c9193b1a241dfd28bf691866db0df5f1",
+			"revisionTime": "2017-12-13T22:16:10Z"
 		},
 		{
 			"checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=",
@@ -126,15 +132,15 @@
 			"checksumSHA1": "X6Q8nYb+KXh+64AKHwWOOcyijHQ=",
 			"origin": "git.autistici.org/id/go-sso/vendor/golang.org/x/crypto/ed25519",
 			"path": "golang.org/x/crypto/ed25519",
-			"revision": "8396b6ffcb3731465f1f0d05e2af4f5b139591b1",
-			"revisionTime": "2017-12-09T17:37:13Z"
+			"revision": "68704340c9193b1a241dfd28bf691866db0df5f1",
+			"revisionTime": "2017-12-13T22:16:10Z"
 		},
 		{
 			"checksumSHA1": "LXFcVx8I587SnWmKycSDEq9yvK8=",
 			"origin": "git.autistici.org/id/go-sso/vendor/golang.org/x/crypto/ed25519/internal/edwards25519",
 			"path": "golang.org/x/crypto/ed25519/internal/edwards25519",
-			"revision": "8396b6ffcb3731465f1f0d05e2af4f5b139591b1",
-			"revisionTime": "2017-12-09T17:37:13Z"
+			"revision": "68704340c9193b1a241dfd28bf691866db0df5f1",
+			"revisionTime": "2017-12-13T22:16:10Z"
 		},
 		{
 			"checksumSHA1": "1MGpGDQqnUoRpv7VEcQrXOBydXE=",