@@ -256,6 +256,9 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
256256if err != nil {
257257return xerrors .Errorf ("determine current snapshot: %w" ,err )
258258}
259+
260+ c .reportHardLimitedPresets (snapshot )
261+
259262if len (snapshot .Presets )== 0 {
260263logger .Debug (ctx ,"no templates found with prebuilds configured" )
261264return nil
@@ -296,6 +299,49 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
296299return err
297300}
298301
302+ func (c * StoreReconciler )reportHardLimitedPresets (snapshot * prebuilds.GlobalSnapshot ) {
303+ // presetsMap is a map from key (orgName:templateName:presetName) to list of corresponding presets.
304+ // Multiple versions of a preset can exist with the same orgName, templateName, and presetName,
305+ // because templates can have multiple versions — or deleted templates can share the same name.
306+ presetsMap := make (map [hardLimitedPresetKey ][]database.GetTemplatePresetsWithPrebuildsRow )
307+ for _ ,preset := range snapshot .Presets {
308+ key := hardLimitedPresetKey {
309+ orgName :preset .OrganizationName ,
310+ templateName :preset .TemplateName ,
311+ presetName :preset .Name ,
312+ }
313+
314+ presetsMap [key ]= append (presetsMap [key ],preset )
315+ }
316+
317+ // Report a preset as hard-limited only if all the following conditions are met:
318+ // - The preset is marked as hard-limited
319+ // - The preset is using the active version of its template, and the template has not been deleted
320+ //
321+ // The second condition is important because a hard-limited preset that has become outdated is no longer relevant.
322+ // Its associated prebuilt workspaces were likely deleted, and it's not meaningful to continue reporting it
323+ // as hard-limited to the admin.
324+ //
325+ // This approach accounts for all relevant scenarios:
326+ // Scenario #1: The admin created a new template version with the same preset names.
327+ // Scenario #2: The admin created a new template version and renamed the presets.
328+ // Scenario #3: The admin deleted a template version that contained hard-limited presets.
329+ //
330+ // In all of these cases, only the latest and non-deleted presets will be reported.
331+ // All other presets will be ignored and eventually removed from Prometheus.
332+ isPresetHardLimited := make (map [hardLimitedPresetKey ]bool )
333+ for key ,presets := range presetsMap {
334+ for _ ,preset := range presets {
335+ if preset .UsingActiveVersion && ! preset .Deleted && snapshot .IsHardLimited (preset .ID ) {
336+ isPresetHardLimited [key ]= true
337+ break
338+ }
339+ }
340+ }
341+
342+ c .metrics .registerHardLimitedPresets (isPresetHardLimited )
343+ }
344+
299345// SnapshotState captures the current state of all prebuilds across templates.
300346func (c * StoreReconciler )SnapshotState (ctx context.Context ,store database.Store ) (* prebuilds.GlobalSnapshot ,error ) {
301347if err := ctx .Err ();err != nil {
@@ -361,24 +407,6 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres
361407slog .F ("preset_name" ,ps .Preset .Name ),
362408)
363409
364- // Report a metric only if the preset uses the latest version of the template and the template is not deleted.
365- // This avoids conflicts between metrics from old and new template versions.
366- //
367- // NOTE: Multiple versions of a preset can exist with the same orgName, templateName, and presetName,
368- // because templates can have multiple versions — or deleted templates can share the same name.
369- //
370- // The safest approach is to report the metric only for the latest version of the preset.
371- // When a new template version is released, the metric for the new preset should overwrite
372- // the old value in Prometheus.
373- //
374- // However, there’s one edge case: if an admin creates a template, it becomes hard-limited,
375- // then deletes the template and never creates another with the same name,
376- // the old preset will continue to be reported as hard-limited —
377- // even though it’s deleted. This will persist until `coderd` is restarted.
378- if ps .Preset .UsingActiveVersion && ! ps .Preset .Deleted {
379- c .metrics .trackHardLimitedStatus (ps .Preset .OrganizationName ,ps .Preset .TemplateName ,ps .Preset .Name ,ps .IsHardLimited )
380- }
381-
382410// If the preset reached the hard failure limit for the first time during this iteration:
383411// - Mark it as hard-limited in the database
384412// - Send notifications to template admins