diff --git a/.changelog/23457.txt b/.changelog/23457.txt
new file mode 100644
index 00000000000..a5e658d2c62
--- /dev/null
+++ b/.changelog/23457.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+scaling: Added `-check-index` support to `job scale` command
+```
diff --git a/api/jobs.go b/api/jobs.go
index 8ac8555f56f..02c6b226da3 100644
--- a/api/jobs.go
+++ b/api/jobs.go
@@ -215,8 +215,7 @@ func (j *Jobs) Info(jobID string, q *QueryOptions) (*Job, *QueryMeta, error) {
 	return &resp, qm, nil
 }
 
-// Scale is used to retrieve information about a particular
-// job given its unique ID.
+// Scale is used to scale a job.
 func (j *Jobs) Scale(jobID, group string, count *int, message string, error bool, meta map[string]interface{},
 	q *WriteOptions) (*JobRegisterResponse, *WriteMeta, error) {
 
@@ -242,6 +241,17 @@ func (j *Jobs) Scale(jobID, group string, count *int, message string, error bool
 	return &resp, qm, nil
 }
 
+// ScaleWithRequest is used to scale a job, giving the caller complete control
+// over the ScalingRequest
+func (j *Jobs) ScaleWithRequest(jobID string, req *ScalingRequest, q *WriteOptions) (*JobRegisterResponse, *WriteMeta, error) {
+	var resp JobRegisterResponse
+	qm, err := j.client.put(fmt.Sprintf("/v1/job/%s/scale", url.PathEscape(jobID)), req, &resp, q)
+	if err != nil {
+		return nil, nil, err
+	}
+	return &resp, qm, nil
+}
+
 // ScaleStatus is used to retrieve information about a particular
 // job given its unique ID.
 func (j *Jobs) ScaleStatus(jobID string, q *QueryOptions) (*JobScaleStatusResponse, *QueryMeta, error) {
diff --git a/api/scaling.go b/api/scaling.go
index e3f49d0363b..cad20bd3fb6 100644
--- a/api/scaling.go
+++ b/api/scaling.go
@@ -57,8 +57,13 @@ type ScalingRequest struct {
 	Error   bool
 	Meta    map[string]interface{}
 	WriteRequest
+
 	// this is effectively a job update, so we need the ability to override policy.
 	PolicyOverride bool
+
+	// If JobModifyIndex is set then the job will only be scaled if it matches
+	// the current Jobs index. The JobModifyIndex is ignored if 0.
+	JobModifyIndex uint64
 }
 
 // ScalingPolicy is the user-specified API object for an autoscaling policy
diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go
index 53d1c0f370e..b12427d9795 100644
--- a/command/agent/job_endpoint.go
+++ b/command/agent/job_endpoint.go
@@ -668,6 +668,7 @@ func (s *HTTPServer) jobScaleAction(resp http.ResponseWriter, req *http.Request,
 		Message:        args.Message,
 		Error:          args.Error,
 		Meta:           args.Meta,
+		JobModifyIndex: args.JobModifyIndex,
 	}
 	// parseWriteRequest overrides Namespace, Region and AuthToken
 	// based on values from the original http request
diff --git a/command/job_scale.go b/command/job_scale.go
index 5daf117c619..626f27c3394 100644
--- a/command/job_scale.go
+++ b/command/job_scale.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/hashicorp/nomad/api"
+	"github.com/hashicorp/nomad/helper/pointer"
 	"github.com/mitchellh/cli"
 	"github.com/posener/complete"
 )
@@ -49,6 +50,11 @@ General Options:
 
 Scale Options:
 
+  -check-index
+    If set, the job is only scaled if the passed job modify index matches the
+    server side version. Ignored if value of zero is passed. If a non-zero value
+    is passed, it ensures that the job is being updated from a known state.
+
   -detach
     Return immediately instead of entering monitor mode. After job scaling,
     the evaluation ID will be printed to the screen, which can be used to
@@ -68,8 +74,9 @@ func (j *JobScaleCommand) Synopsis() string {
 func (j *JobScaleCommand) AutocompleteFlags() complete.Flags {
 	return mergeAutocompleteFlags(j.Meta.AutocompleteFlags(FlagSetClient),
 		complete.Flags{
-			"-detach":  complete.PredictNothing,
-			"-verbose": complete.PredictNothing,
+			"-check-index": complete.PredictNothing,
+			"-detach":      complete.PredictNothing,
+			"-verbose":     complete.PredictNothing,
 		})
 }
 
@@ -79,9 +86,11 @@ func (j *JobScaleCommand) Name() string { return "job scale" }
 // Run satisfies the cli.Command Run function.
 func (j *JobScaleCommand) Run(args []string) int {
 	var detach, verbose bool
+	var checkIndex uint64
 
 	flags := j.Meta.FlagSet(j.Name(), FlagSetClient)
 	flags.Usage = func() { j.Ui.Output(j.Help()) }
+	flags.Uint64Var(&checkIndex, "check-index", 0, "")
 	flags.BoolVar(&detach, "detach", false, "")
 	flags.BoolVar(&verbose, "verbose", false, "")
 	if err := flags.Parse(args); err != nil {
@@ -144,7 +153,18 @@ func (j *JobScaleCommand) Run(args []string) int {
 
 	// Perform the scaling action.
 	w := &api.WriteOptions{Namespace: namespace}
-	resp, _, err := client.Jobs().Scale(jobID, groupString, &count, msg, false, nil, w)
+	req := &api.ScalingRequest{
+		Count: pointer.Of(int64(count)),
+		Target: map[string]string{
+			"Job":   jobID,
+			"Group": groupString,
+		},
+		Message:        msg,
+		PolicyOverride: false,
+		JobModifyIndex: checkIndex,
+	}
+
+	resp, _, err := client.Jobs().ScaleWithRequest(jobID, req, w)
 	if err != nil {
 		j.Ui.Error(fmt.Sprintf("Error submitting scaling request: %s", err))
 		return 1
diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go
index ce75e35d491..d30fa2b6bf3 100644
--- a/nomad/job_endpoint.go
+++ b/nomad/job_endpoint.go
@@ -1094,6 +1094,14 @@ func (j *Job) Scale(args *structs.JobScaleRequest, reply *structs.JobRegisterRes
 			return structs.NewErrRPCCoded(400, "job scaling blocked due to active deployment")
 		}
 
+		// If JobModifyIndex set, check it before trying to apply
+		if args.JobModifyIndex > 0 {
+			if args.JobModifyIndex != job.JobModifyIndex {
+				return fmt.Errorf("%s %d: job exists with conflicting job modify index: %d",
+					RegisterEnforceIndexErrPrefix, args.JobModifyIndex, job.JobModifyIndex)
+			}
+		}
+
 		// Commit the job update
 		_, jobModifyIndex, err := j.srv.raftApply(
 			structs.JobRegisterRequestType,
diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go
index e975fa9b5fd..0595a3327aa 100644
--- a/nomad/job_endpoint_test.go
+++ b/nomad/job_endpoint_test.go
@@ -7749,6 +7749,52 @@ func TestJobEndpoint_Scale_DeploymentBlocking(t *testing.T) {
 	}
 }
 
+func TestJobEndpoint_ScaleEnforceIndex(t *testing.T) {
+	ci.Parallel(t)
+
+	s1, cleanupS1 := TestServer(t, nil)
+	defer cleanupS1()
+	codec := rpcClient(t, s1)
+	testutil.WaitForLeader(t, s1.RPC)
+	store := s1.fsm.State()
+
+	job := mock.Job()
+	originalCount := job.TaskGroups[0].Count
+	err := store.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
+	must.NoError(t, err)
+
+	groupName := job.TaskGroups[0].Name
+	scale := &structs.JobScaleRequest{
+		JobID: job.ID,
+		Target: map[string]string{
+			structs.ScalingTargetGroup: groupName,
+		},
+		Count:   pointer.Of(int64(originalCount + 1)),
+		Message: "because of the load",
+		Meta: map[string]interface{}{
+			"metrics": map[string]string{
+				"1": "a",
+				"2": "b",
+			},
+			"other": "value",
+		},
+		PolicyOverride: false,
+		EnforceIndex:   true,
+		JobModifyIndex: 1000000,
+		WriteRequest: structs.WriteRequest{
+			Region:    "global",
+			Namespace: job.Namespace,
+		},
+	}
+	var resp structs.JobRegisterResponse
+	err = msgpackrpc.CallWithCodec(codec, "Job.Scale", scale, &resp)
+	must.EqError(t, err,
+		"Enforcing job modify index 1000000: job exists with conflicting job modify index: 1000")
+
+	events, _, _ := store.ScalingEventsByJob(nil, job.Namespace, job.ID)
+	must.Len(t, 0, events[groupName])
+}
+
 func TestJobEndpoint_Scale_InformationalEventsShouldNotBeBlocked(t *testing.T) {
 	ci.Parallel(t)
 	require := require.New(t)
diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go
index b5b0a1ec1f8..06b21756143 100644
--- a/nomad/structs/structs.go
+++ b/nomad/structs/structs.go
@@ -859,8 +859,16 @@ type JobScaleRequest struct {
 	Message string
 	Error   bool
 	Meta    map[string]interface{}
+
 	// PolicyOverride is set when the user is attempting to override any policies
 	PolicyOverride bool
+
+	// If EnforceIndex is set then the job will only be scaled if the passed
+	// JobModifyIndex matches the current Jobs index. If the index is zero,
+	// EnforceIndex is ignored.
+	EnforceIndex   bool
+	JobModifyIndex uint64
+
 	WriteRequest
 }
 
diff --git a/website/content/api-docs/jobs.mdx b/website/content/api-docs/jobs.mdx
index 8e7695f9097..5222ceedc8a 100644
--- a/website/content/api-docs/jobs.mdx
+++ b/website/content/api-docs/jobs.mdx
@@ -2389,24 +2389,35 @@ The table below shows this endpoint's support for
 
 - `Count` `(int: <optional>)` - Specifies the new task group count.
 
-- `Target` `(json: required)` - JSON map containing the target of the scaling operation.
-  Must contain a field `Group` with the name of the task group that is the target of this scaling action.
+- `EnforceIndex` `(bool: false)` - If set, the job will only be registered if
+  the passed `JobModifyIndex` matches the current job's index. If the index is
+  zero, the register only occurs if the job is new. This paradigm allows
+  check-and-set style job updating.
 
-- `Message` `(string: <optional>)` - Description of the scale action, persisted as part of the scaling event.
-  Indicates information or reason for scaling; one of `Message` or `Error` must be provided.
+- `Error` `(string: <optional>)` - Description of the scale action, persisted as
+  part of the scaling event.  Indicates an error state preventing scaling; one
+  of `Message` or `Error` must be provided.
 
-- `Error` `(string: <optional>)` - Description of the scale action, persisted as part of the scaling event.
-  Indicates an error state preventing scaling; one of `Message` or `Error` must be provided.
+- `JobModifyIndex` `(int: 0)` - Specifies the `JobModifyIndex` to enforce the
+  current job is at.
 
-- `Meta` `(json: <optional>)` - JSON block that is persisted as part of the scaling event.
+- `Message` `(string: <optional>)` - Description of the scale action, persisted
+  as part of the scaling event.  Indicates information or reason for scaling;
+  one of `Message` or `Error` must be provided.
 
-- `PolicyOverride` `(bool: false)` - If set, any soft mandatory Sentinel policies
-  will be overridden. This allows a job to be scaled when it would be denied
-  by policy.
+- `Meta` `(json: <optional>)` - JSON block that is persisted as part of the scaling event.
 
 - `namespace` `(string: "default")` - Specifies the target namespace. If ACL is
-enabled, this value must match a namespace that the token is allowed to
-access. This is specified as a query string parameter.
+   enabled, this value must match a namespace that the token is allowed to
+   access. This is specified as a query string parameter.
+
+- `PolicyOverride` `(bool: false)` - If set, any soft mandatory Sentinel
+  policies will be overridden. This allows a job to be scaled when it would be
+  denied by policy.
+
+- `Target` `(json: required)` - JSON map containing the target of the scaling
+  operation.  Must contain a field `Group` with the name of the task group that
+  is the target of this scaling action.
 
 ### Sample Payload
 
diff --git a/website/content/docs/commands/job/scale.mdx b/website/content/docs/commands/job/scale.mdx
index f3171dc5f38..0647d7d2090 100644
--- a/website/content/docs/commands/job/scale.mdx
+++ b/website/content/docs/commands/job/scale.mdx
@@ -41,6 +41,11 @@ not used.
 
 ## Scale Options
 
+- `-check-index`: If set, the job is only scaled if the passed job modify index
+  matches the server side version. Ignored if value of zero is passed. If a
+  non-zero value is passed, it ensures that the job is being updated from a
+  known state.
+
 - `-detach`: Return immediately instead of entering monitor mode. After the
   scale command is submitted, a new evaluation ID is printed to the screen,
   which can be used to examine the evaluation using the [eval status] command.