keboola/doctrine-retry-bundle
A Symfony bundle that adds configurable retry logic to Doctrine DBAL connections, helping applications recover from transient database failures and improve resilience with minimal configuration.
^8.4, doctrine/dbal ^4.2, doctrine/doctrine-bundle ^3.0, and Symfony components ^7.4|^8.0. If you are on Symfony 6.4 / doctrine-bundle 2.x you cannot take 1.2.0 at all — stay on 1.1.1 or plan the framework upgrade first. Run composer why-not keboola/doctrine-retry-bundle ^1.2 rather than guessing.config/bundles.php.x_connect_retries to the connection's driverOptions:# config/packages/doctrine.yaml
doctrine:
dbal:
connections:
default:
url: '%env(resolve:DATABASE_URL)%'
driverOptions:
x_connect_retries: 3
x_connect_retries: 0, is skipped entirely — that is the correct way to disable the bundle for one connection without removing it.Where to look first: the bundle README (x_connect_retries is the only public knob) and, in the source, DependencyInjection\Compiler\ConfigureDbalRetryProxyPass — the compiler pass is the real substance of the package, and it tells you exactly which connection keys are read and how the middleware is wired. Database\Retry\Driver shows what is and is not retried.
First use case: an app that fails to boot or serve its first request during a deploy, a MySQL failover, or a transient network blip. Instead of adding retry loops in bootstrap code or in a custom DBAL connection factory, you set one integer and the connection establishment gets retried automatically. This is particularly relevant for ephemeral CI/container environments and for workers that open a single connection at startup and die immediately if the database isn't reachable yet.
Per-connection opt-in, driven by environment. Because the value is read from the connection definition at compile time, you can source it from an env var and keep staging/production different:
driverOptions:
x_connect_retries: '%env(int:DB_CONNECT_RETRIES)%'
Keep it low (2–5). The retry budget multiplies your connection timeout; it is not a substitute for a working healthcheck.
Upgrade the bundle and doctrine-bundle together, then re-verify. 1.2.0 exists specifically to sit on doctrine-bundle 3.x, so the framework bump and the bundle bump happen in the same change. The compiler pass works by rewriting DoctrineBundle's setMiddlewares() method call in the compiled container; a major DoctrineBundle release is exactly the kind of thing that can move that call. Land the upgrade, then immediately run your fault-injection test before merging anything else on top — do not treat "it installed" as "it retries".
Retry connect, retry transactions yourself. The driver overrides only connect(). That is deliberate: replaying a query after a mid-transaction failure corrupts state. For deadlocks and serialization failures, keep an application-level retry around the whole transaction closure (Doctrine's transactional() plus your own retry helper), with idempotency guarantees. The two layers are complementary, not overlapping — do not expect this bundle to save a flaky query.
Tune it differently for web vs. CLI. The short-lived web request wants a small number of quick attempts; a migration or a messenger:consume worker boot can afford more. Use separate connections (or separate env values per process type) rather than a single global compromise.
Multiple connections and replicas. The pass walks every entry in doctrine.connections, so you can enable retries on the primary write connection only, or on read replicas independently. Remember that Configuration middleware is shared where the connections share a configuration service — verify behaviour per connection rather than assuming isolation.
Middleware ordering matters, and the bundle handles it. The retry middleware is prepended, so it wraps the driver outermost and sees failures raised by every other middleware. This means middleware you add (logging, profiling, custom instrumentation) sits inside the retry loop: with DBAL logging enabled you will typically see one logged failure per attempt, which is exactly what you want for alerting on "how often do we actually retry".
Verify the path with a real fault, not a mock. The upstream test suite uses Toxiproxy to cut the connection and force the failure path, plus a FailingRetryProxy alias to count attempts deterministically. In your own app, the cheap equivalent is pointing the connection at a closed port and asserting the request takes roughly attempts × connect timeout and that your logs show N attempts. For a permanent regression guard, alias/replace the retry proxy service in config/services_test.yaml with a counting stub and assert it was invoked. This test is worth more after 1.2.0 than it was before it.
Health checks and readiness probes. Since a broken database now costs you N connect attempts, make sure your readiness endpoint uses a very short timeout or x_connect_retries: 0; otherwise probes stack up during an outage and slow down orchestration decisions.
setMiddlewares() method call (removes it, re-adds it with the retry middleware prepended). If DoctrineBundle changed how middlewares are registered, the pass finds nothing to rewrite and your retries disappear with no error, no warning, no deprecation. 1.2.0 is the first release targeting doctrine-bundle 3.x, so if you are moving from 1.1.1 + doctrine-bundle 2.x, treat re-verification of the wiring as a required step of the upgrade, not an optional one. Keep doing this for every subsequent doctrine/doctrine-bundle or Symfony major.^1.1.1) so a future resolution doesn't try to drag the bundle forward and fail the install, and be aware that 1.1.1 will not receive the Symfony 8 fixes.driverOptions is forwarded to the real driver. x_connect_retries is a synthetic key sitting in the options array that DBAL passes down to PDO. Confirm with your driver/PHP version that the key is stripped before delegation or tolerated; a smoke test that just opens a connection on each of your configured drivers is worth the five minutes — re-run it after the DBAL 4.2 / doctrine-bundle 3 bump, since driver option handling is one of the things that moves between majors."3" versus 3 can matter; use %env(int:...)% or an unquoted integer so the pass doesn't see something it treats as absent.var/cache/dev/App_KernelDevDebugContainer.xml) and look for the Configuration service's setMiddlewares call — the middleware list should contain a per-connection retry middleware. bin/console debug:container plus a cache clear is the fastest way to confirm a config change actually reached the pass, since compile-time passes do not react to runtime config edits.^8.4, doctrine/dbal ^4.2, doctrine-bundle ^3.0, Symfony ^7.4|^8.0. DBAL 3 and doctrine-bundle 2.x are not supported, and the package will not install on the LTS stack most apps still run. Check composer why-not before assuming a conflict is your app's fault.FailingRetryProxy). Replacing the compiler pass entirely is the other option if you have non-standard middleware registration.keboola/doctrine-retry-bundle; changes go to libs/doctrine-retry-bundle/ in keboola/platform-libraries and are force-pushed over the mirror. If you need a patch, vendor a fork or pin a commit from the monorepo path.x_connect_retries, and budget time to read the four classes rather than searching for blog posts. The upside is that the entire surface area is small enough to audit in one sitting, which is the right thing to do before it sits in your critical boot path.How can I help you explore Laravel packages today?