diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 590c474776..c902b80942 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -35,11 +35,13 @@ import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/ import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard'; +import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard'; import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard'; import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; +import { ThrottlerException } from 'src/engine/core-modules/throttler/throttler.exception'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -348,6 +350,37 @@ export abstract class CommonBaseQueryRunnerService< } private async throttleQueryExecution(authContext: WorkspaceAuthContext) { + await this.throttleApiKeyQueryExecution(authContext); + await this.throttleApplicationQueryExecution(authContext); + } + + private async throttleApplicationQueryExecution( + authContext: WorkspaceAuthContext, + ) { + if (!isApplicationAuthContext(authContext)) return; + + try { + await this.throttlerService.tokenBucketThrottleOrThrow( + `api:throttler:application:${authContext.application.universalIdentifier}`, + 1, + this.twentyConfigService.get('APPLICATION_API_RATE_LIMITING_LIMIT'), + this.twentyConfigService.get('APPLICATION_API_RATE_LIMITING_TTL_IN_MS'), + ); + } catch (error) { + if (error instanceof ThrottlerException) { + await this.metricsService.incrementCounterForEvent({ + key: MetricsKeys.CommonApiApplicationQueryRateLimited, + shouldStoreInCache: false, + }); + } + + throw error; + } + } + + private async throttleApiKeyQueryExecution( + authContext: WorkspaceAuthContext, + ) { try { if (!isApiKeyAuthContext(authContext)) return; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts index 5fc29782d7..37adbad8d6 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts @@ -94,7 +94,7 @@ export class CronTriggerCronJob { payload: {}, }, ], - { retryLimit: 3 }, + { retryLimit: 10 }, ); } } catch (error) { diff --git a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts index 8d189bc227..7150db9695 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts @@ -52,6 +52,7 @@ export enum MetricsKeys { JobWebhookCallCompleted = 'job/webhook-call-completed', SignUpSuccess = 'sign-up/success', CommonApiQueryRateLimited = 'common-api-query/rate-limited', + CommonApiApplicationQueryRateLimited = 'common-api-query/application-rate-limited', JobCompleted = 'job/completed', JobFailed = 'job/failed', JobStalled = 'job/stalled', diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index 453d49907a..c282133799 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -1483,6 +1483,24 @@ export class ConfigVariables { @CastToPositiveNumber() API_RATE_LIMITING_LONG_LIMIT = 100; + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.RATE_LIMITING, + description: + 'Time-to-live for per-application API rate limiting in milliseconds', + type: ConfigVariableType.NUMBER, + }) + @CastToPositiveNumber() + APPLICATION_API_RATE_LIMITING_TTL_IN_MS = 60_000; + + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.RATE_LIMITING, + description: + 'Maximum number of API requests allowed per application across all workspaces in the rate limiting window', + type: ConfigVariableType.NUMBER, + }) + @CastToPositiveNumber() + APPLICATION_API_RATE_LIMITING_LIMIT = 500; + @CastToPositiveNumber() @ConfigVariablesMetadata({ group: ConfigVariablesGroup.RATE_LIMITING,