Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0326ff1
feat(dashboard): Display environment queue length limits on queues an…
ericallam Jan 30, 2026
be8fa57
Make it clear the limit is across all queues in the env
ericallam Jan 30, 2026
4918f83
A couple of devin improvements and adding an in memory cache for the …
ericallam Jan 31, 2026
9aac1c4
Add queue length limits at the queue level, lazy waitpoint creation, …
ericallam Feb 5, 2026
e93dbba
Failed batch queue processing now creates a pre-failed run, better ha…
ericallam Feb 9, 2026
45b6cdb
introduce maximum ttl via the RUN_ENGINE_DEFAULT_MAX_TTL optional env…
ericallam Feb 9, 2026
28d955c
fix webapp typecheck issue
ericallam Feb 9, 2026
6004591
improve efficiency of expiring runs in batch, and make sure runs are …
ericallam Feb 10, 2026
5eee2df
Make sure maxTtl is enforced even when the ttl option passed in does …
ericallam Feb 10, 2026
17aabd4
Create a more reliable ttl expiration system using atomic redis worker
ericallam Feb 10, 2026
037826b
queue size limits are upgradable and don't make the max dev queue hav…
ericallam Feb 11, 2026
441386f
correctly clear runs from env current concurrency sets when dequeued …
ericallam Feb 11, 2026
d424754
Only engage the ttl system when a run is first enqueued, no longer on…
ericallam Feb 11, 2026
7c143dd
Ensure lazy waitpoints are always created and completed even if the e…
ericallam Feb 11, 2026
471bf19
Adopt the pre-failed run approach in the legacy run engine batch trig…
ericallam Feb 11, 2026
4b612ee
We don't need these sdk changes anymore because of the different way …
ericallam Feb 12, 2026
5bfeac4
Remove unrelated queue metrics design doc
ericallam Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,9 @@ const EnvironmentSchema = z

MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional(),
MAXIMUM_DEPLOYED_QUEUE_SIZE: z.coerce.number().int().optional(),
QUEUE_SIZE_CACHE_TTL_MS: z.coerce.number().int().optional().default(1_000), // 1 second
QUEUE_SIZE_CACHE_MAX_SIZE: z.coerce.number().int().optional().default(5_000),
QUEUE_SIZE_CACHE_ENABLED: z.coerce.number().int().optional().default(1),
MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500),
MAX_BATCH_AND_WAIT_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500),

Expand Down Expand Up @@ -596,6 +599,16 @@ const EnvironmentSchema = z
RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_JITTER_IN_MS: z.coerce.number().int().optional(),
RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_JITTER_IN_MS: z.coerce.number().int().optional(),

// TTL System settings for automatic run expiration
RUN_ENGINE_TTL_SYSTEM_DISABLED: BoolEnv.default(false),
RUN_ENGINE_TTL_SYSTEM_SHARD_COUNT: z.coerce.number().int().optional(),
RUN_ENGINE_TTL_SYSTEM_POLL_INTERVAL_MS: z.coerce.number().int().default(1_000),
RUN_ENGINE_TTL_SYSTEM_BATCH_SIZE: z.coerce.number().int().default(100),

/** Optional maximum TTL for all runs (e.g. "14d"). If set, runs without an explicit TTL
* will use this as their TTL, and runs with a TTL larger than this will be clamped. */
RUN_ENGINE_DEFAULT_MAX_TTL: z.string().optional(),

RUN_ENGINE_RUN_LOCK_DURATION: z.coerce.number().int().default(5000),
RUN_ENGINE_RUN_LOCK_AUTOMATIC_EXTENSION_THRESHOLD: z.coerce.number().int().default(1000),
RUN_ENGINE_RUN_LOCK_MAX_RETRIES: z.coerce.number().int().default(10),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 = {
Expand All @@ -9,6 +10,7 @@ export type Environment = {
concurrencyLimit: number;
burstFactor: number;
runsEnabled: boolean;
queueSizeLimit: number | null;
};

export class EnvironmentQueuePresenter extends BasePresenter {
Expand All @@ -30,19 +32,24 @@ export class EnvironmentQueuePresenter extends BasePresenter {
},
select: {
runsEnabled: true,
maximumDevQueueSize: true,
maximumDeployedQueueSize: true,
},
});

if (!organization) {
throw new Error("Organization not found");
}

const queueSizeLimit = getQueueSizeLimit(environment.type, organization);

return {
running,
queued,
concurrencyLimit: environment.maximumConcurrencyLimit,
burstFactor: environment.concurrencyLimitBurstFactor.toNumber(),
runsEnabled: environment.type === "DEVELOPMENT" || organization.runsEnabled,
queueSizeLimit,
};
}
}
144 changes: 83 additions & 61 deletions apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,6 +13,8 @@ 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";
import { getQueueSizeLimit, getQueueSizeLimitSource } from "~/v3/utils/queueLimits.server";

// Create a singleton Redis client for rate limit queries
const rateLimitRedisClient = singleton("rateLimitQueryRedisClient", () =>
Expand Down Expand Up @@ -66,8 +69,7 @@ export type LimitsResult = {
logRetentionDays: QuotaInfo | null;
realtimeConnections: QuotaInfo | null;
batchProcessingConcurrency: QuotaInfo;
devQueueSize: QuotaInfo;
deployedQueueSize: QuotaInfo;
queueSize: QuotaInfo;
};
features: {
hasStagingEnvironment: FeatureInfo;
Expand All @@ -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<LimitsResult> {
// Get organization with all limit-related fields
Expand Down Expand Up @@ -167,6 +171,30 @@ export class LimitsPresenter extends BasePresenter {
batchRateLimitConfig
);

// 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,
maximumConcurrencyLimit: true,
concurrencyLimitBurstFactor: true,
},
});

let currentQueueSize = 0;
if (runtimeEnv) {
const engineEnv = {
id: runtimeEnv.id,
type: environmentType,
maximumConcurrencyLimit: runtimeEnv.maximumConcurrencyLimit,
concurrencyLimitBurstFactor: runtimeEnv.concurrencyLimitBurstFactor,
organization: { id: organizationId },
project: { id: projectId },
};
currentQueueSize = (await engine.lengthOfEnvQueue(engineEnv)) ?? 0;
}

// Get plan-level limits
const schedulesLimit = limits?.schedules?.number ?? null;
const teamMembersLimit = limits?.teamMembers?.number ?? null;
Expand Down Expand Up @@ -206,72 +234,72 @@ export class LimitsPresenter extends BasePresenter {
schedules:
schedulesLimit !== null
? {
name: "Schedules",
description: "Maximum number of schedules per project",
limit: schedulesLimit,
currentUsage: scheduleCount,
source: "plan",
canExceed: limits?.schedules?.canExceed,
isUpgradable: true,
}
name: "Schedules",
description: "Maximum number of schedules per project",
limit: schedulesLimit,
currentUsage: scheduleCount,
source: "plan",
canExceed: limits?.schedules?.canExceed,
isUpgradable: true,
}
: null,
teamMembers:
teamMembersLimit !== null
? {
name: "Team members",
description: "Maximum number of team members in this organization",
limit: teamMembersLimit,
currentUsage: organization._count.members,
source: "plan",
canExceed: limits?.teamMembers?.canExceed,
isUpgradable: true,
}
name: "Team members",
description: "Maximum number of team members in this organization",
limit: teamMembersLimit,
currentUsage: organization._count.members,
source: "plan",
canExceed: limits?.teamMembers?.canExceed,
isUpgradable: true,
}
: null,
alerts:
alertsLimit !== null
? {
name: "Alert channels",
description: "Maximum number of alert channels per project",
limit: alertsLimit,
currentUsage: alertChannelCount,
source: "plan",
canExceed: limits?.alerts?.canExceed,
isUpgradable: true,
}
name: "Alert channels",
description: "Maximum number of alert channels per project",
limit: alertsLimit,
currentUsage: alertChannelCount,
source: "plan",
canExceed: limits?.alerts?.canExceed,
isUpgradable: true,
}
: null,
branches:
branchesLimit !== null
? {
name: "Preview branches",
description: "Maximum number of active preview branches per project",
limit: branchesLimit,
currentUsage: activeBranchCount,
source: "plan",
canExceed: limits?.branches?.canExceed,
isUpgradable: true,
}
name: "Preview branches",
description: "Maximum number of active preview branches per project",
limit: branchesLimit,
currentUsage: activeBranchCount,
source: "plan",
canExceed: limits?.branches?.canExceed,
isUpgradable: true,
}
: null,
logRetentionDays:
logRetentionDaysLimit !== null
? {
name: "Log retention",
description: "Number of days logs are retained",
limit: logRetentionDaysLimit,
currentUsage: 0, // Not applicable - this is a duration, not a count
source: "plan",
}
name: "Log retention",
description: "Number of days logs are retained",
limit: logRetentionDaysLimit,
currentUsage: 0, // Not applicable - this is a duration, not a count
source: "plan",
}
: null,
realtimeConnections:
realtimeConnectionsLimit !== null
? {
name: "Realtime connections",
description: "Maximum concurrent Realtime connections",
limit: realtimeConnectionsLimit,
currentUsage: 0, // Would need to query realtime service for this
source: "plan",
canExceed: limits?.realtimeConcurrentConnections?.canExceed,
isUpgradable: true,
}
name: "Realtime connections",
description: "Maximum concurrent Realtime connections",
limit: realtimeConnectionsLimit,
currentUsage: 0, // Would need to query realtime service for this
source: "plan",
canExceed: limits?.realtimeConcurrentConnections?.canExceed,
isUpgradable: true,
}
: null,
batchProcessingConcurrency: {
name: "Batch processing concurrency",
Expand All @@ -282,19 +310,13 @@ 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 queued runs",
description: "Maximum pending runs per individual queue in this environment",
limit: getQueueSizeLimit(environmentType, organization),
currentUsage: currentQueueSize,
source: getQueueSizeLimitSource(environmentType, organization),
isUpgradable: true,
},
},
features: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
);
Expand Down Expand Up @@ -507,9 +508,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 (
<div className="flex flex-col gap-3">
Expand Down Expand Up @@ -556,9 +556,12 @@ function QuotaRow({
billingPath: string;
}) {
// For log retention, we don't show current usage as it's a duration, not a count
// For queue size, we don't show current usage as the limit is per-queue, not environment-wide
const isRetentionQuota = quota.name === "Log retention";
const isQueueSizeQuota = quota.name === "Max queued runs";
const hideCurrentUsage = isRetentionQuota || isQueueSizeQuota;
const percentage =
!isRetentionQuota && quota.limit && quota.limit > 0 ? quota.currentUsage / quota.limit : null;
!hideCurrentUsage && quota.limit && quota.limit > 0 ? quota.currentUsage / quota.limit : null;

// Special handling for Log retention
if (quota.name === "Log retention") {
Expand Down Expand Up @@ -657,10 +660,10 @@ function QuotaRow({
alignment="right"
className={cn(
"tabular-nums",
isRetentionQuota ? "text-text-dimmed" : getUsageColorClass(percentage, "usage")
hideCurrentUsage ? "text-text-dimmed" : getUsageColorClass(percentage, "usage")
)}
>
{isRetentionQuota ? "–" : formatNumber(quota.currentUsage)}
{hideCurrentUsage ? "–" : formatNumber(quota.currentUsage)}
</TableCell>
<TableCell alignment="right">
<SourceBadge source={quota.source} />
Expand Down
Loading