Skip to content

perf(auth): lazily load auth drivers to reduce memory usage - #28146

Open
Vansh1811 wants to merge 6 commits into
directus:mainfrom
Vansh1811:perf/lazy-load-auth-drivers
Open

perf(auth): lazily load auth drivers to reduce memory usage#28146
Vansh1811 wants to merge 6 commits into
directus:mainfrom
Vansh1811:perf/lazy-load-auth-drivers

Conversation

@Vansh1811

Copy link
Copy Markdown

Auth drivers for LDAP, OAuth2, OpenID, and SAML pull in heavyweight dependencies (ldapjs, openid-client, @node-saml/node-saml, etc.) that were previously imported eagerly at the top of api/src/auth.ts. This meant every one of these packages was loaded into memory on every server boot, even when only the local provider was configured.

This change replaces the static imports with dynamic import() calls inside getProviderInstance, so only the drivers that are actually referenced by AUTH_PROVIDERS get loaded.

Fixes #24334

What's Changed

  • Removed static top-level imports of LDAPAuthDriver, LocalAuthDriver, OAuth2AuthDriver, OpenIDAuthDriver, and SAMLAuthDriver in api/src/auth.ts
  • getProviderInstance is now async and dynamically imports only the driver module matching the requested type
  • registerAuthProviders awaits the dynamic instantiation for the default provider and each configured provider

Tested Scenarios

  • No AUTH_PROVIDERS configured: only local driver module is loaded
  • AUTH_PROVIDERS with ldap/oauth2/openid/saml configured: verified respective driver modules load correctly and authentication still functions as expected

Refactor auth provider registration to use async/await for dynamic imports and provider instance creAuth drivers for LDAP, OAuth2, OpenID, and SAML pull in heavyweight dependencies (ldapjs, openid-client, @node-saml/node-saml, etc.) that were previously imported eagerly at the top of api/src/auth.ts. This meant every one of these packages was loaded into memory on every server boot, even when only the local provider was configured.

This change replaces the static imports with dynamic import() calls inside getProviderInstance, so only the drivers that are actually referenced by AUTH_PROVIDERS get loaded.

Fixes directus#24334
@github-actions github-actions Bot added the CLA Required Contributor License Agreement not signed label Aug 24, 2026
@github-actions github-actions Bot removed the CLA Required Contributor License Agreement not signed label Aug 24, 2026
Reduced memory usage on startup by lazily loading LDAP, OAuth2, OpenID, and SAML auth drivers via dynamic imports, ensuring dependencies are loaded only when configured via AUTH_PROVIDERS.
Add tests for dynamic loading of authentication providers.

@ComfortablyCoding ComfortablyCoding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Please see my comments below.

In addition, please ensure the PR template is followed. As this is a performance-related change, please provide stats demonstrating that it provides a meaningful improvement.

Comment thread api/src/auth.ts
Comment on lines +75 to +82
/**
* Lazily imports and instantiates the auth driver matching the given type.
*
* Auth drivers (LDAP, OAuth2, OpenID, SAML) pull in heavyweight dependencies
* (ldapjs, openid-client, etc). Dynamically importing them here ensures that
* only the drivers actually configured via AUTH_PROVIDERS get loaded into
* memory, instead of all drivers being loaded eagerly on every boot.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify this

Suggested change
/**
* Lazily imports and instantiates the auth driver matching the given type.
*
* Auth drivers (LDAP, OAuth2, OpenID, SAML) pull in heavyweight dependencies
* (ldapjs, openid-client, etc). Dynamically importing them here ensures that
* only the drivers actually configured via AUTH_PROVIDERS get loaded into
* memory, instead of all drivers being loaded eagerly on every boot.
*/
/**
* Lazily loads the auth driver for the given type.
*/

Comment thread api/src/auth.ts
Comment on lines -5 to -11
import {
LDAPAuthDriver,
LocalAuthDriver,
OAuth2AuthDriver,
OpenIDAuthDriver,
SAMLAuthDriver,
} from './auth/drivers/index.js';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this handles lazy instantiation, the drivers are still statically imported in the controller

Comment thread api/src/auth.ts

// Always register default provider
const defaultProvider = getProviderInstance('local', options)!;
const defaultProvider = await getProviderInstance('local', options)!;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! is asserting the promise, not the value, is still seen as possibly undefined by TS

Comment thread api/src/auth.test.ts
expect(samlDriverCtor).not.toHaveBeenCalled();
});

test('registerAuthProviders dynamically imports the oauth2 driver when configured', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first test, this one and the next 2 essentially all test the same flow, let's condense them down to one.

Comment thread api/src/auth.test.ts
Comment on lines +70 to +94
test('registerAuthProviders only dynamically imports drivers that are configured', async () => {
mockEnv['AUTH_PROVIDERS'] = 'ldap_provider';

const { registerAuthProviders, getAuthProvider } = await import('./auth.js');

await registerAuthProviders();

expect(ldapDriverCtor).toHaveBeenCalledTimes(1);
expect(oauth2DriverCtor).not.toHaveBeenCalled();
expect(openidDriverCtor).not.toHaveBeenCalled();
expect(samlDriverCtor).not.toHaveBeenCalled();
expect(getAuthProvider('ldap_provider')).toEqual({ type: 'ldap' });
expect(getAuthProvider('default')).toEqual({ type: 'local' });
});

test('registerAuthProviders does not import unused drivers when AUTH_PROVIDERS is unset', async () => {
const { registerAuthProviders } = await import('./auth.js');

await registerAuthProviders();

expect(ldapDriverCtor).not.toHaveBeenCalled();
expect(oauth2DriverCtor).not.toHaveBeenCalled();
expect(openidDriverCtor).not.toHaveBeenCalled();
expect(samlDriverCtor).not.toHaveBeenCalled();
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These (and subsequent) tests do not assert lazy loading is taking place, only that the constructor is not instantiating. Any possible regression of the lazy load will therefor not be caught.

Comment thread api/src/auth.ts

// Register configured providers
providerNames.forEach((name: string) => {
for (let name of providerNames) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will load them sequentially, no need in this case, let's load them in parallel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Packages using memory even if they are not used

2 participants