From a68ef306e8b3a716539c4619eb16265f70b009ca Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 30 Jan 2026 19:01:34 +0000 Subject: [PATCH 1/3] feat(dashboard): Display environment queue length limits on queues and limits page --- .../v3/EnvironmentQueuePresenter.server.ts | 10 +++ .../presenters/v3/LimitsPresenter.server.ts | 61 ++++++++++++++----- .../route.tsx | 5 +- .../route.tsx | 36 ++++++++++- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts index f408511a83..e8b1461515 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts @@ -1,3 +1,4 @@ +import { env } from "~/env.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { marqs } from "~/v3/marqs/index.server"; import { engine } from "~/v3/runEngine.server"; @@ -9,6 +10,7 @@ export type Environment = { concurrencyLimit: number; burstFactor: number; runsEnabled: boolean; + queueSizeLimit: number | null; }; export class EnvironmentQueuePresenter extends BasePresenter { @@ -30,6 +32,8 @@ export class EnvironmentQueuePresenter extends BasePresenter { }, select: { runsEnabled: true, + maximumDevQueueSize: true, + maximumDeployedQueueSize: true, }, }); @@ -37,12 +41,18 @@ export class EnvironmentQueuePresenter extends BasePresenter { throw new Error("Organization not found"); } + const queueSizeLimit = + environment.type === "DEVELOPMENT" + ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) + : (organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null); + return { running, queued, concurrencyLimit: environment.maximumConcurrencyLimit, burstFactor: environment.concurrencyLimitBurstFactor.toNumber(), runsEnabled: environment.type === "DEVELOPMENT" || organization.runsEnabled, + queueSizeLimit, }; } } diff --git a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts index 11b66d6c0b..e5e09ad5bc 100644 --- a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts @@ -12,6 +12,7 @@ import { BasePresenter } from "./basePresenter.server"; import { singleton } from "~/utils/singleton"; import { logger } from "~/services/logger.server"; import { CheckScheduleService } from "~/v3/services/checkSchedule.server"; +import { engine } from "~/v3/runEngine.server"; // Create a singleton Redis client for rate limit queries const rateLimitRedisClient = singleton("rateLimitQueryRedisClient", () => @@ -66,8 +67,7 @@ export type LimitsResult = { logRetentionDays: QuotaInfo | null; realtimeConnections: QuotaInfo | null; batchProcessingConcurrency: QuotaInfo; - devQueueSize: QuotaInfo; - deployedQueueSize: QuotaInfo; + queueSize: QuotaInfo; }; features: { hasStagingEnvironment: FeatureInfo; @@ -167,6 +167,32 @@ export class LimitsPresenter extends BasePresenter { batchRateLimitConfig ); + // Get current queue size for this environment + const runtimeEnv = await this._replica.runtimeEnvironment.findFirst({ + where: { id: environmentId }, + select: { + id: true, + type: true, + organizationId: true, + projectId: true, + maximumConcurrencyLimit: true, + concurrencyLimitBurstFactor: true, + }, + }); + + let currentQueueSize = 0; + if (runtimeEnv) { + const engineEnv = { + id: runtimeEnv.id, + type: runtimeEnv.type, + maximumConcurrencyLimit: runtimeEnv.maximumConcurrencyLimit, + concurrencyLimitBurstFactor: runtimeEnv.concurrencyLimitBurstFactor, + organization: { id: runtimeEnv.organizationId }, + project: { id: runtimeEnv.projectId }, + }; + currentQueueSize = (await engine.lengthOfEnvQueue(engineEnv)) ?? 0; + } + // Get plan-level limits const schedulesLimit = limits?.schedules?.number ?? null; const teamMembersLimit = limits?.teamMembers?.number ?? null; @@ -282,19 +308,24 @@ export class LimitsPresenter extends BasePresenter { canExceed: true, isUpgradable: true, }, - devQueueSize: { - name: "Dev queue size", - description: "Maximum pending runs in development environments", - limit: organization.maximumDevQueueSize ?? null, - currentUsage: 0, // Would need to query Redis for this - source: organization.maximumDevQueueSize ? "override" : "default", - }, - deployedQueueSize: { - name: "Deployed queue size", - description: "Maximum pending runs in deployed environments", - limit: organization.maximumDeployedQueueSize ?? null, - currentUsage: 0, // Would need to query Redis for this - source: organization.maximumDeployedQueueSize ? "override" : "default", + queueSize: { + name: "Max queue size", + description: "Maximum pending runs in this environment", + limit: + runtimeEnv?.type === "DEVELOPMENT" + ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) + : (organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null), + currentUsage: currentQueueSize, + // "plan" = org has a value (typically set by billing sync) + // "default" = no org value, using env var fallback + source: + runtimeEnv?.type === "DEVELOPMENT" + ? organization.maximumDevQueueSize + ? "plan" + : "default" + : organization.maximumDeployedQueueSize + ? "plan" + : "default", }, }, features: { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx index dfaffe9938..c979f096de 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx @@ -507,9 +507,8 @@ function QuotasSection({ // Include batch processing concurrency quotaRows.push(quotas.batchProcessingConcurrency); - // Add queue size quotas if set - if (quotas.devQueueSize.limit !== null) quotaRows.push(quotas.devQueueSize); - if (quotas.deployedQueueSize.limit !== null) quotaRows.push(quotas.deployedQueueSize); + // Add queue size quota if set + if (quotas.queueSize.limit !== null) quotaRows.push(quotas.queueSize); return (
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 3a8a7544c5..523b80be68 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -68,6 +68,7 @@ import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePrese import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; +import { formatNumberCompact } from "~/utils/numberFormatter"; import { concurrencyPath, docsPath, @@ -345,7 +346,27 @@ export default function Page() { 0 ? "paused" : undefined} + suffix={ + environment.queueSizeLimit ? ( + + / + + {formatNumberCompact(environment.queueSizeLimit)} + + + + ) : env.paused && environment.queued > 0 ? ( + "paused" + ) : undefined + } animate accessory={
@@ -364,7 +385,10 @@ export default function Page() { />
} - valueClassName={env.paused ? "text-warning" : undefined} + valueClassName={ + getQueueUsageColorClass(environment.queued, environment.queueSizeLimit) ?? + (env.paused ? "text-warning" : undefined) + } compactThreshold={1000000} /> ); } + +function getQueueUsageColorClass(current: number, limit: number | null): string | undefined { + if (!limit) return undefined; + const percentage = current / limit; + if (percentage >= 1) return "text-error"; + if (percentage >= 0.9) return "text-warning"; + return undefined; +} From a0f94ffb041bc2c6d84435735c287fcc277d66b7 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 30 Jan 2026 19:09:36 +0000 Subject: [PATCH 2/3] Make it clear the limit is across all queues in the env --- apps/webapp/app/presenters/v3/LimitsPresenter.server.ts | 4 ++-- .../route.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts index e5e09ad5bc..acc0fbe5fd 100644 --- a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts @@ -309,8 +309,8 @@ export class LimitsPresenter extends BasePresenter { isUpgradable: true, }, queueSize: { - name: "Max queue size", - description: "Maximum pending runs in this environment", + name: "Max queued runs", + description: "Maximum pending runs across all queues in this environment", limit: runtimeEnv?.type === "DEVELOPMENT" ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 523b80be68..80b005cfc1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -359,7 +359,7 @@ export default function Page() { {formatNumberCompact(environment.queueSizeLimit)} From a50a5f56957c3b73d43dac916ec117718d6a331d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 31 Jan 2026 09:03:15 +0000 Subject: [PATCH 3/3] A couple of devin improvements and adding an in memory cache for the env queue size check --- apps/webapp/app/env.server.ts | 4 +- .../v3/EnvironmentQueuePresenter.server.ts | 7 +- .../presenters/v3/LimitsPresenter.server.ts | 30 +++------ .../route.tsx | 1 + .../route.tsx | 66 +++++++++++++------ .../app/runEngine/concerns/queues.server.ts | 40 ++++++++--- .../webapp/app/v3/utils/queueLimits.server.ts | 51 ++++++++++++++ 7 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 apps/webapp/app/v3/utils/queueLimits.server.ts diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index dcbcac079a..da77f7973b 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -533,8 +533,10 @@ const EnvironmentSchema = z BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(262_144), // 256KB - MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional(), + MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional().default(500), MAXIMUM_DEPLOYED_QUEUE_SIZE: z.coerce.number().int().optional(), + QUEUE_SIZE_CACHE_TTL_MS: z.coerce.number().int().optional().default(30_000), // 30 seconds + QUEUE_SIZE_CACHE_MAX_SIZE: z.coerce.number().int().optional().default(5_000), MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), MAX_BATCH_AND_WAIT_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), diff --git a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts index e8b1461515..1020109437 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts @@ -1,7 +1,7 @@ -import { env } from "~/env.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { marqs } from "~/v3/marqs/index.server"; import { engine } from "~/v3/runEngine.server"; +import { getQueueSizeLimit } from "~/v3/utils/queueLimits.server"; import { BasePresenter } from "./basePresenter.server"; export type Environment = { @@ -41,10 +41,7 @@ export class EnvironmentQueuePresenter extends BasePresenter { throw new Error("Organization not found"); } - const queueSizeLimit = - environment.type === "DEVELOPMENT" - ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) - : (organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null); + const queueSizeLimit = getQueueSizeLimit(environment.type, organization); return { running, diff --git a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts index acc0fbe5fd..5a169b02c9 100644 --- a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts @@ -1,4 +1,5 @@ import { Ratelimit } from "@upstash/ratelimit"; +import { RuntimeEnvironmentType } from "@trigger.dev/database"; import { createHash } from "node:crypto"; import { env } from "~/env.server"; import { getCurrentPlan } from "~/services/platform.v3.server"; @@ -13,6 +14,7 @@ import { singleton } from "~/utils/singleton"; import { logger } from "~/services/logger.server"; import { CheckScheduleService } from "~/v3/services/checkSchedule.server"; import { engine } from "~/v3/runEngine.server"; +import { getQueueSizeLimit, getQueueSizeLimitSource } from "~/v3/utils/queueLimits.server"; // Create a singleton Redis client for rate limit queries const rateLimitRedisClient = singleton("rateLimitQueryRedisClient", () => @@ -84,11 +86,13 @@ export class LimitsPresenter extends BasePresenter { organizationId, projectId, environmentId, + environmentType, environmentApiKey, }: { organizationId: string; projectId: string; environmentId: string; + environmentType: RuntimeEnvironmentType; environmentApiKey: string; }): Promise { // Get organization with all limit-related fields @@ -168,13 +172,11 @@ export class LimitsPresenter extends BasePresenter { ); // Get current queue size for this environment + // We need the runtime environment fields for the engine query const runtimeEnv = await this._replica.runtimeEnvironment.findFirst({ where: { id: environmentId }, select: { id: true, - type: true, - organizationId: true, - projectId: true, maximumConcurrencyLimit: true, concurrencyLimitBurstFactor: true, }, @@ -184,11 +186,11 @@ export class LimitsPresenter extends BasePresenter { if (runtimeEnv) { const engineEnv = { id: runtimeEnv.id, - type: runtimeEnv.type, + type: environmentType, maximumConcurrencyLimit: runtimeEnv.maximumConcurrencyLimit, concurrencyLimitBurstFactor: runtimeEnv.concurrencyLimitBurstFactor, - organization: { id: runtimeEnv.organizationId }, - project: { id: runtimeEnv.projectId }, + organization: { id: organizationId }, + project: { id: projectId }, }; currentQueueSize = (await engine.lengthOfEnvQueue(engineEnv)) ?? 0; } @@ -311,21 +313,9 @@ export class LimitsPresenter extends BasePresenter { queueSize: { name: "Max queued runs", description: "Maximum pending runs across all queues in this environment", - limit: - runtimeEnv?.type === "DEVELOPMENT" - ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) - : (organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null), + limit: getQueueSizeLimit(environmentType, organization), currentUsage: currentQueueSize, - // "plan" = org has a value (typically set by billing sync) - // "default" = no org value, using env var fallback - source: - runtimeEnv?.type === "DEVELOPMENT" - ? organization.maximumDevQueueSize - ? "plan" - : "default" - : organization.maximumDeployedQueueSize - ? "plan" - : "default", + source: getQueueSizeLimitSource(environmentType, organization), }, }, features: { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx index c979f096de..b6fcf2cef5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx @@ -82,6 +82,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, + environmentType: environment.type, environmentApiKey: environment.apiKey, }) ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 80b005cfc1..41f573692a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -347,25 +347,11 @@ export default function Page() { title="Queued" value={environment.queued} suffix={ - environment.queueSizeLimit ? ( - - / - - {formatNumberCompact(environment.queueSizeLimit)} - - - - ) : env.paused && environment.queued > 0 ? ( - "paused" - ) : undefined + } animate accessory={ @@ -1150,3 +1136,45 @@ function getQueueUsageColorClass(current: number, limit: number | null): string if (percentage >= 0.9) return "text-warning"; return undefined; } + +/** + * Renders the suffix for the Queued BigNumber, showing: + * - The limit with usage color and tooltip (if queueSizeLimit is set) + * - "paused" text (if environment is paused) + * - Both indicators when applicable + */ +function QueuedSuffix({ + queued, + queueSizeLimit, + isPaused, +}: { + queued: number; + queueSizeLimit: number | null; + isPaused: boolean; +}) { + const showLimit = queueSizeLimit !== null; + + if (!showLimit && !isPaused) { + return null; + } + + return ( + + {showLimit && ( + <> + / + + {formatNumberCompact(queueSizeLimit)} + + + + )} + {isPaused && ( + {showLimit ? "(paused)" : "paused"} + )} + + ); +} diff --git a/apps/webapp/app/runEngine/concerns/queues.server.ts b/apps/webapp/app/runEngine/concerns/queues.server.ts index 0980dc2a75..611e51a3d9 100644 --- a/apps/webapp/app/runEngine/concerns/queues.server.ts +++ b/apps/webapp/app/runEngine/concerns/queues.server.ts @@ -15,6 +15,22 @@ import type { RunEngine } from "~/v3/runEngine.server"; import { env } from "~/env.server"; import { tryCatch } from "@trigger.dev/core/v3"; import { ServiceValidationError } from "~/v3/services/common.server"; +import { createCache, createLRUMemoryStore, DefaultStatefulContext, Namespace } from "@internal/cache"; +import { singleton } from "~/utils/singleton"; + +// LRU cache for environment queue sizes to reduce Redis calls +const queueSizeCache = singleton("queueSizeCache", () => { + const ctx = new DefaultStatefulContext(); + const memory = createLRUMemoryStore(env.QUEUE_SIZE_CACHE_MAX_SIZE, "queue-size-cache"); + + return createCache({ + queueSize: new Namespace(ctx, { + stores: [memory], + fresh: env.QUEUE_SIZE_CACHE_TTL_MS, + stale: env.QUEUE_SIZE_CACHE_TTL_MS + 1000, + }), + }); +}); /** * Extract the queue name from a queue option that may be: @@ -49,7 +65,7 @@ export class DefaultQueueManager implements QueueManager { constructor( private readonly prisma: PrismaClientOrTransaction, private readonly engine: RunEngine - ) {} + ) { } async resolveQueueProperties( request: TriggerTaskRequest, @@ -75,8 +91,7 @@ export class DefaultQueueManager implements QueueManager { if (!specifiedQueue) { throw new ServiceValidationError( - `Specified queue '${specifiedQueueName}' not found or not associated with locked version '${ - lockedBackgroundWorker.version ?? "" + `Specified queue '${specifiedQueueName}' not found or not associated with locked version '${lockedBackgroundWorker.version ?? "" }'.` ); } @@ -98,8 +113,7 @@ export class DefaultQueueManager implements QueueManager { if (!lockedTask) { throw new ServiceValidationError( - `Task '${request.taskId}' not found on locked version '${ - lockedBackgroundWorker.version ?? "" + `Task '${request.taskId}' not found on locked version '${lockedBackgroundWorker.version ?? "" }'.` ); } @@ -113,8 +127,7 @@ export class DefaultQueueManager implements QueueManager { version: lockedBackgroundWorker.version, }); throw new ServiceValidationError( - `Default queue configuration for task '${request.taskId}' missing on locked version '${ - lockedBackgroundWorker.version ?? "" + `Default queue configuration for task '${request.taskId}' missing on locked version '${lockedBackgroundWorker.version ?? "" }'.` ); } @@ -282,7 +295,7 @@ async function guardQueueSizeLimitsForEnv( return { isWithinLimits: true }; } - const queueSize = await engine.lengthOfEnvQueue(environment); + const queueSize = await getCachedQueueSize(engine, environment); const projectedSize = queueSize + itemsToAdd; return { @@ -291,3 +304,14 @@ async function guardQueueSizeLimitsForEnv( queueSize, }; } + +async function getCachedQueueSize( + engine: RunEngine, + environment: AuthenticatedEnvironment +): Promise { + const result = await queueSizeCache.queueSize.swr(environment.id, async () => { + return engine.lengthOfEnvQueue(environment); + }); + + return result.val ?? 0; +} diff --git a/apps/webapp/app/v3/utils/queueLimits.server.ts b/apps/webapp/app/v3/utils/queueLimits.server.ts new file mode 100644 index 0000000000..5cefc7e0a6 --- /dev/null +++ b/apps/webapp/app/v3/utils/queueLimits.server.ts @@ -0,0 +1,51 @@ +import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { env } from "~/env.server"; + +/** + * Organization fields needed for queue limit calculation. + */ +export type QueueLimitOrganization = { + maximumDevQueueSize: number | null; + maximumDeployedQueueSize: number | null; +}; + +/** + * Calculates the queue size limit for an environment based on its type and organization settings. + * + * Resolution order: + * 1. Organization-level override (set by billing sync or admin) + * 2. Environment variable fallback + * 3. null if neither is set + * + * @param environmentType - The type of the runtime environment + * @param organization - Organization with queue limit fields + * @returns The queue size limit, or null if unlimited + */ +export function getQueueSizeLimit( + environmentType: RuntimeEnvironmentType, + organization: QueueLimitOrganization +): number | null { + if (environmentType === "DEVELOPMENT") { + return organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null; + } + + return organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null; +} + +/** + * Determines the source of the queue size limit for display purposes. + * + * @param environmentType - The type of the runtime environment + * @param organization - Organization with queue limit fields + * @returns "plan" if org has a value (typically set by billing), "default" if using env var fallback + */ +export function getQueueSizeLimitSource( + environmentType: RuntimeEnvironmentType, + organization: QueueLimitOrganization +): "plan" | "default" { + if (environmentType === "DEVELOPMENT") { + return organization.maximumDevQueueSize !== null ? "plan" : "default"; + } + + return organization.maximumDeployedQueueSize !== null ? "plan" : "default"; +}