Repository: apache/trafficcontrol
Pull request #6569 (2022-02-16 16:18:26)

T3C Race Condition Update
<!--
Thank you for contributing! Please be sure to read our contribution guidelines: https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
If this closes or relates to an existing issue, please reference it using one of the following:

Closes: #ISSUE
Related: #ISSUE

If this PR fixes a security vulnerability, DO NOT submit! Instead, contact
the Apache Traffic Control Security Team at security@trafficcontrol.apache.org and follow the
guidelines at https://apache.org/security regarding vulnerability disclosure.
-->

There currently exists a scenario, and happens in production environments, where the use of a boolean flag alone causes a race condition when setting/unsetting the `upd_pending` and `reval_pending` flags associated with servers.


1. Cache config (T3C) checks and sees the queue flag is set (Say it's `false`)
2. Cache config (T3C) starts running 
3. Updates are queued in Traffic Ops (Set flag to `true`)
4. Cache config (T3C) un-sets the flag (Set flag back to `false`)
5. The new update is not applied.


To address this race condition, rather than relying on a boolean flag, the use of timestamps will help ensure the last time either _update_ or _revalidate_ requests were set and/or applied. The boolean flags have been deprecated as of TO v3.1 API and removed for v4.0.

Most notably, 4 fields will be added when queuing update/revals. As well as CRUD related calls involving _servers_. These four fields are:

```json
"configUpdateTime":"1969-12-31T17:00:00-07:00"
"configApplyTime":"1969-12-31T17:00:00-07:00"
"revalUpdateTime":"1969-12-31T17:00:00-07:00"
"revalApplyTime":"1969-12-31T17:00:00-07:00"
```

Pending configuration and revalidation updates will be calculated based on a comparison of the timestamps. For _queueing_ and _dequeueing_ updates for groups of servers (say via Delivery Services or CDNs), the `update` and `apply` times are set to be equivalent, thus showing no pending updates or revalidations.

<!-- **^ Add meaningful description above** --><hr/>

## Which Traffic Control components are affected by this PR?
<!-- Please delete all components from this list that are NOT affected by this PR.
Feel free to add the name of a tool or script that is affected but not on the list.
-->
- Documentation
- Traffic Control Cache Config (T3C, formerly ORT)
- Traffic Control Client (Go clients)
- Traffic Ops

## What is the best way to verify this PR?
<!-- Please include here ALL the steps necessary to test your PR.
If your PR has tests (and most should), provide the steps needed to run the tests.
If not, please provide step-by-step instructions to test the PR manually and explain why your PR does not need tests. -->

`upd_pending` and `reval_pending` flags will continue to be served. They will be calculated based upon the timestamp values.

Specific endpoints that should be validated for v3.x and v4.x:

GET `/servers/{hostname}/update_status`
v3.x response **MUST** not contain the new timestamp fields
v4.x resposne **MUST** contain the new timestamp fields

POST `servers/{id-or-name}/update`
v3.x request **MUST** continue to accept and work with `upd_pending` and `reval_pending` flags
v4.x request **MUST** no longer accept `upd_pending` and `reval_pending` flags, but accept valid RFC3339Nano timestamps for `config_apply_time` and/or `revalidate_apply_time`

As expected, all unit tests **MUST** pass, all API integration tests **MUST** pass, and all T3C cache config integration tests **MUST** pass.

If desired, manual testing by hitting the endpoints for /server (GET, PUT, POST, and DEL) to ensure the correct validation logic is performed with or without the added fields to the request. All responses should contain the 4 new fields as well as the two original `upd_pending` and `reval_pending` flags.

## If this is a bugfix, which Traffic Control versions contained the bug?
<!-- Delete this section if the PR is not a bugfix, or if the bug is only in the master branch.
Examples:
- 5.1.2
- 5.1.3 (RC1)
 -->


## PR submission checklist
- [x] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
- [x] This PR has documentation - Documentation may or may not be included in this first PR
- [x] This PR has a CHANGELOG.md entry <!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
- [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
-->


ocket8888: I restarted CiaB and t3c tests; both fail consistently. I can't tell from the limited logs of the CiaB why it's failing for sure, but I'd bet anything it's because t3c is crashing, for the same reason its tests are failing - stack trace:
```
    to_updater_test.go:40: t3c-request failed: exit status 2: stdout:  stderr: INFO: config.go:202: 2022-03-08T05:12:18.575778433Z: loading old config took 400ns
        INFO: config.go:179: 2022-03-08T05:12:18.575806233Z: using old config for IMS requests
        INFO: t3c-request.go:46: 2022-03-08T05:12:18.575813432Z: configuration initialized
        INFO: client.go:75: 2022-03-08T05:12:18.575820332Z: URL: 'https://to_server:443' User: 'admin' Pass len: '6'
        INFO: client.go:83: 2022-03-08T05:12:18.57596943Z: TO URL string: 'https://to_server:443'
        INFO: client.go:84: 2022-03-08T05:12:18.57603403Z: TO URL: 'https://to_server:443'
        INFO: client.go:91: 2022-03-08T05:12:18.576473124Z: Logging in to Traffic Ops '[172.18.0.5]' with Cookie
        INFO: getdata.go:79: 2022-03-08T05:12:18.576502024Z: Getting data 'update-status'
        panic: interface conversion: interface {} is *tc.ServerUpdateStatus, not *tc.ServerUpdateStatusV4
        
        goroutine 1 [running]:
        github.com/apache/trafficcontrol/cache-config/t3cutil/toreq.(*TOClient).GetServerUpdateStatus.func1({0x705aa0, 0xc00026da00})
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3cutil/toreq/clientfuncs.go:729 +0x4d7
        github.com/apache/trafficcontrol/cache-config/t3cutil/toreq/torequtil.GetRetry(0x0, {0xc0000a2c60, 0x24}, {0x705aa0, 0xc00026da00}, 0xc00021f678)
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3cutil/toreq/torequtil/torequtil.go:52 +0x147
        github.com/apache/trafficcontrol/cache-config/t3cutil/toreq.(*TOClient).GetServerUpdateStatus(0xc00009cee8, {0x7ffd64f0296d, 0xf}, 0x0)
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3cutil/toreq/clientfuncs.go:724 +0x19c
        github.com/apache/trafficcontrol/cache-config/t3cutil.GetServerUpdateStatus({{0x7ffd64f0296d, 0xf}, {0x7ffd64f02994, 0xd}, 0xc00009cee8, 0x1, 0xb2d05e00, {0x7ffd64f0292c, 0x6}, {0x7ffd64f0290f, ...}, ...})
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3cutil/getdata.go:71 +0x8c
        github.com/apache/trafficcontrol/cache-config/t3cutil.WriteServerUpdateStatus({{0x7ffd64f0296d, 0xf}, {0x7ffd64f02994, 0xd}, 0xc00009cee8, 0x1, 0xb2d05e00, {0x7ffd64f0292c, 0x6}, {0x7ffd64f0290f, ...}, ...}, ...)
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3cutil/getdata.go:133 +0x59
        github.com/apache/trafficcontrol/cache-config/t3cutil.WriteData({{0x7ffd64f0296d, 0xf}, {0x7ffd64f02994, 0xd}, 0xc00009cee8, 0x1, 0xb2d05e00, {0x7ffd64f0292c, 0x6}, {0x7ffd64f0290f, ...}, ...})
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3cutil/getdata.go:84 +0x3a2
        main.main()
        	/tmp/go/src/github.com/apache/trafficcontrol/cache-config/t3c-request/t3c-request.go:66 +0x31e
    types.go:58: ---- DeleteTestTypes ----
    ```

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1336e7ee..2b8ed4591 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Replaces all Traffic Portal Tenant select boxes with a novel tree select box [#6427](https://github.com/apache/trafficcontrol/issues/6427).
 - Traffic Monitor: Add support for `access.log` to TM.
 - Added functionality for login to provide a Bearer token and for that token to be later used for authorization.
+- [Traffic Ops | Traffic Go Clients | T3C] Add additional timestamp fields to server for queuing and dequeueing config and revalidate updates.
 
 ### Fixed
 - Update traffic\_portal dependencies to mitigate `npm audit` issues.
diff --git a/cache-config/t3c-apply/torequest/cmd.go b/cache-config/t3c-apply/torequest/cmd.go
index b37702c7f..74b80da0b 100644
--- a/cache-config/t3c-apply/torequest/cmd.go
+++ b/cache-config/t3c-apply/torequest/cmd.go
@@ -38,6 +38,7 @@ import (
 
 	"github.com/apache/trafficcontrol/cache-config/t3c-apply/config"
 	"github.com/apache/trafficcontrol/cache-config/t3cutil"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
@@ -190,8 +191,8 @@ func getChkconfig(cfg config.Cfg) ([]map[string]string, error) {
 	return result, nil
 }
 
-func getUpdateStatus(cfg config.Cfg) (*tc.ServerUpdateStatus, error) {
-	status := tc.ServerUpdateStatus{}
+func getUpdateStatus(cfg config.Cfg) (*atscfg.ServerUpdateStatus, error) {
+	status := atscfg.ServerUpdateStatus{}
 	if err := requestJSON(cfg, "update-status", &status); err != nil {
 		return nil, errors.New("requesting json: " + err.Error())
 	}
@@ -216,7 +217,7 @@ func getPackages(cfg config.Cfg) ([]Package, error) {
 
 // sendUpdate updates the given cache's queue update and reval status in Traffic Ops.
 // Note the statuses are the value to be set, not whether to set the value.
-func sendUpdate(cfg config.Cfg, updateStatus bool, revalStatus bool) error {
+func sendUpdate(cfg config.Cfg, configApplyTime, revalApplyTime *time.Time, configApplyBool, revalApplyBool *bool) error {
 	args := []string{
 		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(cfg.TOTimeoutMS), 10),
 		"--traffic-ops-user=" + cfg.TOUser,
@@ -224,10 +225,24 @@ func sendUpdate(cfg config.Cfg, updateStatus bool, revalStatus bool) error {
 		"--traffic-ops-url=" + cfg.TOURL,
 		"--traffic-ops-insecure=" + strconv.FormatBool(cfg.TOInsecure),
 		"--cache-host-name=" + cfg.CacheHostName,
-		"--set-update-status=" + strconv.FormatBool(updateStatus),
-		"--set-reval-status=" + strconv.FormatBool(revalStatus),
 	}
 
+	if configApplyTime != nil {
+		args = append(args, "--set-config-apply-time="+(*configApplyTime).Format(time.RFC3339Nano))
+	}
+	if revalApplyTime != nil {
+		args = append(args, "--set-reval-apply-time="+(*revalApplyTime).Format(time.RFC3339Nano))
+	}
+
+	// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+	if configApplyBool != nil {
+		args = append(args, "--set-update-status="+strconv.FormatBool(*configApplyBool))
+	}
+	if revalApplyBool != nil {
+		args = append(args, "--set-reval-status="+strconv.FormatBool(*revalApplyBool))
+	}
+	// ***
+
 	if cfg.LogLocationErr == log.LogLocationNull {
 		args = append(args, "-s")
 	}
@@ -238,6 +253,7 @@ func sendUpdate(cfg config.Cfg, updateStatus bool, revalStatus bool) error {
 		args = append(args, "-v")
 	}
 
+	// TODO: Do these override the values set above? These appear to be the same, dups?
 	if _, used := os.LookupEnv("TO_USER"); !used {
 		args = append(args, "--traffic-ops-user="+cfg.TOUser)
 	}
diff --git a/cache-config/t3c-apply/torequest/torequest.go b/cache-config/t3c-apply/torequest/torequest.go
index 4cc3bff31..4dbadfa14 100644
--- a/cache-config/t3c-apply/torequest/torequest.go
+++ b/cache-config/t3c-apply/torequest/torequest.go
@@ -1151,7 +1151,7 @@ func (r *TrafficOpsReq) StartServices(syncdsUpdate *UpdateStatus) error {
 }
 
 func (r *TrafficOpsReq) UpdateTrafficOps(syncdsUpdate *UpdateStatus) error {
-	var updateResult bool
+	var performUpdate bool
 
 	serverStatus, err := getUpdateStatus(r.Cfg)
 	if err != nil {
@@ -1159,7 +1159,7 @@ func (r *TrafficOpsReq) UpdateTrafficOps(syncdsUpdate *UpdateStatus) error {
 	}
 
 	if *syncdsUpdate == UpdateTropsNotNeeded && (serverStatus.UpdatePending == true || serverStatus.RevalPending == true) {
-		updateResult = true
+		performUpdate = true
 		log.Errorln("Traffic Ops is signaling that an update is ready to be applied but, none was found! Clearing update state in Traffic Ops anyway.")
 	} else if *syncdsUpdate == UpdateTropsNotNeeded {
 		log.Errorln("Traffic Ops does not require an update at this time")
@@ -1168,11 +1168,11 @@ func (r *TrafficOpsReq) UpdateTrafficOps(syncdsUpdate *UpdateStatus) error {
 		log.Errorln("Traffic Ops requires an update but, applying the update locally failed.  Traffic Ops is not being updated.")
 		return nil
 	} else if *syncdsUpdate == UpdateTropsSuccessful {
-		updateResult = true
+		performUpdate = true
 		log.Errorln("Traffic Ops requires an update and it was applied successfully.  Clearing update state in Traffic Ops.")
 	}
 
-	if !updateResult {
+	if !performUpdate {
 		return nil
 	}
 	if r.Cfg.ReportOnly {
@@ -1180,19 +1180,14 @@ func (r *TrafficOpsReq) UpdateTrafficOps(syncdsUpdate *UpdateStatus) error {
 		return nil
 	}
 
+	// TODO: The boolean flags/representation can be removed after ATC (v7.0+)
 	if !r.Cfg.ReportOnly && !r.Cfg.NoUnsetUpdateFlag {
 		if r.Cfg.Files == t3cutil.ApplyFilesFlagAll {
-			if serverStatus.RevalPending {
-				err = sendUpdate(r.Cfg, false, true)
-			} else {
-				err = sendUpdate(r.Cfg, false, false)
-			}
+			b := false
+			err = sendUpdate(r.Cfg, serverStatus.ConfigUpdateTime, nil, &b, nil)
 		} else if r.Cfg.Files == t3cutil.ApplyFilesFlagReval {
-			if serverStatus.UpdatePending {
-				err = sendUpdate(r.Cfg, true, false)
-			} else {
-				err = sendUpdate(r.Cfg, false, false)
-			}
+			b := false
+			err = sendUpdate(r.Cfg, nil, serverStatus.RevalidateUpdateTime, nil, &b)
 		}
 		if err != nil {
 			return errors.New("Traffic Ops Update failed: " + err.Error())
diff --git a/cache-config/t3c-update/README.md b/cache-config/t3c-update/README.md
index c8de9d0e9..a8e8dab61 100644
--- a/cache-config/t3c-update/README.md
+++ b/cache-config/t3c-update/README.md
@@ -53,9 +53,15 @@ t3c-update [-ahIqv] [-d value] [-e value] [-H value] [-i value] [-l value] [-P v
 
 # OPTIONS
 
--a, -\-set-reval-status
+-q, -\-set-config-apply-time
 
-    [true | false] sets the servers revalidate status (required)
+    [RFC3339Nano Timestamp] sets the server's config apply time.
+    Either this or set-reval-apply-time must be used (Required)
+
+-a, -\-set-reval-apply-time
+
+    [RFC3339Nano Timestamp] sets the server's reval apply time.
+    Either this or set-config-apply-time must be used (Required)
 
 -H, -\-cache-host-name=value
 
@@ -80,10 +86,6 @@ t3c-update [-ahIqv] [-d value] [-e value] [-H value] [-i value] [-l value] [-P v
     Traffic Ops password. Required. May also be set with the
     environment variable TO_PASS
 
--q, -\-set-update-status
-
-    [true | false] sets the servers update status (required)
-
 -s, -\-silent
 
     Silent. Errors are not logged, and the 'verbose' flag is
@@ -116,6 +118,18 @@ t3c-update [-ahIqv] [-d value] [-e value] [-H value] [-i value] [-l value] [-P v
 
     Print the version and exit
 
+-y, -\-set-update-status
+
+    [true or nonexistent] Set the Update Status to false for the server.
+    Compatability requirement until ATC (v7.0+) is deployed 
+    with the timestamp features.
+
+-z, -\-set-reval-status
+
+    [true or nonexistent] Set the Reval Status to false for the server.
+    Compatability requirement until ATC (v7.0+) is deployed 
+    with the timestamp features.
+
 # AUTHORS
 
 The t3c application is maintained by Apache Traffic Control project. For help, bug reports, contributing, or anything else, see:
diff --git a/cache-config/t3c-update/config/config.go b/cache-config/t3c-update/config/config.go
index 8e0f9ce59..05df10afd 100644
--- a/cache-config/t3c-update/config/config.go
+++ b/cache-config/t3c-update/config/config.go
@@ -43,8 +43,10 @@ type Cfg struct {
 	LoginDispersion  time.Duration
 	CacheHostName    string
 	GetData          string
-	UpdatePending    bool
-	RevalPending     bool
+	ConfigApplyTime  *time.Time
+	RevalApplyTime   *time.Time
+	ConfigApplyBool  *bool
+	RevalApplyBool   *bool
 	t3cutil.TCCfg
 	Version     string
 	GitRevision string
@@ -69,10 +71,10 @@ func Usage() {
 func InitConfig(appVersion string, gitRevision string) (Cfg, error) {
 	dispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds between 0 and [seconds] before login to traffic ops, default 0")
 	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
-	const setUpdateStatusFlagName = "set-update-status"
-	updatePendingPtr := getopt.BoolLong(setUpdateStatusFlagName, 'q', "[true | false] sets the servers update status")
-	const setRevalStatusFlagName = "set-reval-status"
-	revalPendingPtr := getopt.BoolLong(setRevalStatusFlagName, 'a', "[true | false] sets the servers revalidate status")
+	const setConfigApplyTimeFlagName = "set-config-apply-time"
+	configApplyTimeStringPtr := getopt.StringLong(setConfigApplyTimeFlagName, 'q', "", "[RFC3339Nano Timestamp] sets the server's config apply time")
+	const setRevalApplyTimeFlagName = "set-reval-apply-time"
+	revalApplyTimeStringPtr := getopt.StringLong(setRevalApplyTimeFlagName, 'a', "", "[RFC3339Nano Timestamp] sets the server's reval apply time")
 	toInsecurePtr := getopt.BoolLong("traffic-ops-insecure", 'I', "[true | false] ignore certificate errors from Traffic Ops")
 	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
 	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with     the environment variable TO_URL")
@@ -83,6 +85,13 @@ func InitConfig(appVersion string, gitRevision string) (Cfg, error) {
 	verbosePtr := getopt.CounterLong("verbose", 'v', `Log verbosity. Logging is output to stderr. By default, errors are logged. To log warnings, pass '-v'. To log info, pass '-vv'. To omit error logging, see '-s'`)
 	silentPtr := getopt.BoolLong("silent", 's', `Silent. Errors are not logged, and the 'verbose' flag is ignored. If a fatal error occurs, the return code will be non-zero but no text will be output to stderr`)
 
+	// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+	const setConfigApplyBoolFlagName = "set-update-status"
+	configApplyBoolFlag := getopt.BoolLong(setConfigApplyBoolFlagName, 'y', `[false or nonexistent] Set the Update Status to false for the server`)
+	const setRevalApplyBoolFlagName = "set-reval-status"
+	revalApplyBoolFlag := getopt.BoolLong(setRevalApplyBoolFlagName, 'z', `[false or nonexistent] Set the Reval Status to false for the server`)
+	// ***
+
 	getopt.Parse()
 
 	if *helpPtr == true {
@@ -93,14 +102,57 @@ func InitConfig(appVersion string, gitRevision string) (Cfg, error) {
 		os.Exit(0)
 	}
 
-	if !getopt.IsSet(setRevalStatusFlagName) {
-		fmt.Println("--set-reval-status is required")
+	// Verify at least one flag is passed
+	if (!getopt.IsSet(setConfigApplyTimeFlagName) && !getopt.IsSet(setRevalApplyTimeFlagName)) &&
+		(!getopt.IsSet(setConfigApplyBoolFlagName) && !getopt.IsSet(setRevalApplyBoolFlagName)) { // TODO: Remove once ATC (v7.0+) is deployed
+		fmt.Printf("Must set either %s or %s. One is at least required.\n", setConfigApplyTimeFlagName, setRevalApplyTimeFlagName)
 		os.Exit(0)
 	}
-	if !getopt.IsSet(setUpdateStatusFlagName) {
-		fmt.Printf("DEBUG %v\n", *updatePendingPtr)
-		fmt.Println("--set-update-status is required")
-		os.Exit(0)
+
+	var configApplyTimePtr, revalApplyTimePtr *time.Time
+	// Validate that it can be parsed to a valid timestamp
+	if getopt.IsSet(setConfigApplyTimeFlagName) {
+		parsed, err := time.Parse(time.RFC3339Nano, *configApplyTimeStringPtr)
+		if err != nil {
+			fmt.Printf("%s must be a valid RFC3339Nano timestamp", setConfigApplyTimeFlagName)
+		}
+		configApplyTimePtr = &parsed
+	}
+	if getopt.IsSet(setRevalApplyTimeFlagName) {
+		parsed, err := time.Parse(time.RFC3339Nano, *revalApplyTimeStringPtr)
+		if err != nil {
+			fmt.Printf("%s must be a valid RFC3339Nano timestamp", setRevalApplyTimeFlagName)
+		}
+		revalApplyTimePtr = &parsed
+	}
+
+	// TODO: Remove once ATC (v7.0+) is deployed
+	var configApplyBoolPtr, revalApplyBoolPtr *bool
+	if getopt.IsSet(setConfigApplyBoolFlagName) {
+		if *configApplyBoolFlag {
+			fmt.Println("set-update-status must be false or nonexistent")
+			os.Exit(0)
+		}
+		configApplyBoolPtr = configApplyBoolFlag
+	} else {
+		configApplyBoolPtr = nil
+	}
+	if getopt.IsSet(setRevalApplyBoolFlagName) {
+		if *revalApplyBoolFlag {
+			fmt.Println("set-reval-status must be false or nonexistent")
+			os.Exit(0)
+		}
+		revalApplyBoolPtr = revalApplyBoolFlag
+	} else {
+		revalApplyBoolPtr = nil
+	}
+
+	// Booleans must trump time for backwards compatibility
+	if configApplyTimePtr != nil {
+		configApplyBoolPtr = nil
+	}
+	if revalApplyTimePtr != nil {
+		revalApplyBoolPtr = nil
 	}
 
 	logLocationError := log.LogLocationStderr
@@ -165,8 +217,10 @@ func InitConfig(appVersion string, gitRevision string) (Cfg, error) {
 		LogLocationInfo:  logLocationInfo,
 		LogLocationWarn:  logLocationWarn,
 		LoginDispersion:  dispersion,
-		UpdatePending:    *updatePendingPtr,
-		RevalPending:     *revalPendingPtr,
+		ConfigApplyTime:  configApplyTimePtr,
+		RevalApplyTime:   revalApplyTimePtr,
+		ConfigApplyBool:  configApplyBoolPtr,
+		RevalApplyBool:   revalApplyBoolPtr,
 		TCCfg: t3cutil.TCCfg{
 			CacheHostName: cacheHostName,
 			GetData:       "update-status",
diff --git a/cache-config/t3c-update/t3c-update.go b/cache-config/t3c-update/t3c-update.go
index 31f9884ef..6b14cbaf3 100644
--- a/cache-config/t3c-update/t3c-update.go
+++ b/cache-config/t3c-update/t3c-update.go
@@ -22,6 +22,7 @@ package main
 import (
 	"fmt"
 	"os"
+	"time"
 
 	"github.com/apache/trafficcontrol/cache-config/t3c-update/config"
 	"github.com/apache/trafficcontrol/cache-config/t3cutil"
@@ -63,7 +64,9 @@ func main() {
 		log.Warnln("Traffic Ops does not support the latest version supported by this app! Falling back to previous major Traffic Ops API version!")
 	}
 
-	err = t3cutil.SetUpdateStatus(cfg.TCCfg, tc.CacheName(cfg.TCCfg.CacheHostName), cfg.UpdatePending, cfg.RevalPending)
+	// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+	// Use SetUpdateStatus is preferred
+	err = t3cutil.SetUpdateStatusCompat(cfg.TCCfg, tc.CacheName(cfg.TCCfg.CacheHostName), cfg.ConfigApplyTime, cfg.RevalApplyTime, cfg.ConfigApplyBool, cfg.RevalApplyBool)
 	if err != nil {
 		log.Errorf("%s, %s\n", err, cfg.TCCfg.CacheHostName)
 		os.Exit(3)
@@ -75,10 +78,16 @@ func main() {
 		os.Exit(4)
 	}
 
-	if cur_status.UpdatePending != cfg.UpdatePending && cfg.RevalPending != cfg.RevalPending {
-		log.Errorf("ERROR: update failed, update status and/or reval status was not set.\n")
-	} else {
-		log.Infoln("Update successfully completed")
+	// When comparing equality, it must be done with microsecond precision (Round not Truncate).
+	// This is because Postgres stores Microsecond precision. Round also drops the monotonic
+	// clock reading.
+	// t3c (Nano) -> client (Nano) -> TO (Nano) -> Postgres (Micro)
+	// Postgres (Micro) -> TO (Micro) -> client (Micro) -> here / t3c (Micro)
+	if cfg.ConfigApplyTime != nil && !(*cfg.ConfigApplyTime).Round(time.Microsecond).Equal((*cur_status.ConfigApplyTime).Round(time.Microsecond)) {
+		log.Errorf("Failed to set config_apply_time.\nSent: %v\nRecv: %v", *cfg.ConfigApplyTime, *cur_status.ConfigApplyTime)
+	}
+	if cfg.RevalApplyTime != nil && !(*cfg.RevalApplyTime).Round(time.Microsecond).Equal((*cur_status.RevalidateApplyTime).Round(time.Microsecond)) {
+		log.Errorf("Failed to set reval_apply_time.\nSent: %v\nRecv: %v", *cfg.RevalApplyTime, *cur_status.RevalidateApplyTime)
 	}
 	cfg.TCCfg.TOClient.WriteFsCookie(toreq.FsCookiePath + cfg.TOUser + ".cookie")
 }
diff --git a/cache-config/t3cutil/getdata.go b/cache-config/t3cutil/getdata.go
index 65bea25fc..419cb8db0 100644
--- a/cache-config/t3cutil/getdata.go
+++ b/cache-config/t3cutil/getdata.go
@@ -67,7 +67,7 @@ func GetDataFuncs() map[string]func(TCCfg, io.Writer) error {
 	}
 }
 
-func GetServerUpdateStatus(cfg TCCfg) (*tc.ServerUpdateStatus, error) {
+func GetServerUpdateStatus(cfg TCCfg) (*atscfg.ServerUpdateStatus, error) {
 	status, _, err := cfg.TOClient.GetServerUpdateStatus(tc.CacheName(cfg.CacheHostName), nil)
 	if err != nil {
 		return nil, errors.New("getting server '" + cfg.CacheHostName + "' update status: " + err.Error())
@@ -219,20 +219,24 @@ type ChkConfigEntry struct {
 }
 
 // SetUpdateStatus sets the queue and reval status of serverName in Traffic Ops.
-func SetUpdateStatus(cfg TCCfg, serverName tc.CacheName, queue bool, revalPending bool) error {
+func SetUpdateStatus(cfg TCCfg, serverName tc.CacheName, configApply, revalApply *time.Time) error {
 	// TODO need to move to toreq, add fallback
-	reqInf, err := cfg.TOClient.SetServerUpdateStatus(serverName, &queue, &revalPending)
+	reqInf, err := cfg.TOClient.SetServerUpdateStatus(serverName, configApply, revalApply)
 	if err != nil {
 		return errors.New("setting update statuses (Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "'): " + err.Error())
 	}
 	return nil
 }
 
-func jsonBoolStr(b bool) string {
-	if b {
-		return `true`
+// SetUpdateStatusCompat sets the queue and reval status of serverName in Traffic Ops.
+// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+func SetUpdateStatusCompat(cfg TCCfg, serverName tc.CacheName, configApply, revalApply *time.Time, configApplyBool, revalApplyBool *bool) error {
+	// TODO need to move to toreq, add fallback
+	reqInf, err := cfg.TOClient.SetServerUpdateStatusBoolCompat(serverName, configApply, revalApply, configApplyBool, revalApplyBool)
+	if err != nil {
+		return errors.New("setting update statuses (Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "'): " + err.Error())
 	}
-	return `false`
+	return nil
 }
 
 // WriteConfig writes the Traffic Ops data necessary to generate config to output.
diff --git a/cache-config/t3cutil/toreq/clientfuncs.go b/cache-config/t3cutil/toreq/clientfuncs.go
index c65b3840d..5c227087c 100644
--- a/cache-config/t3cutil/toreq/clientfuncs.go
+++ b/cache-config/t3cutil/toreq/clientfuncs.go
@@ -29,6 +29,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apache/trafficcontrol/cache-config/t3cutil/toreq/torequtil"
 	"github.com/apache/trafficcontrol/lib/go-atscfg"
@@ -714,41 +715,82 @@ func (cl *TOClient) GetStatuses(reqHdr http.Header) ([]tc.Status, toclientlib.Re
 }
 
 // GetServerUpdateStatus returns the data, the Traffic Ops address, and any error.
-func (cl *TOClient) GetServerUpdateStatus(cacheHostName tc.CacheName, reqHdr http.Header) (tc.ServerUpdateStatus, toclientlib.ReqInf, error) {
+func (cl *TOClient) GetServerUpdateStatus(cacheHostName tc.CacheName, reqHdr http.Header) (atscfg.ServerUpdateStatus, toclientlib.ReqInf, error) {
 	if cl.c == nil {
 		return cl.old.GetServerUpdateStatus(cacheHostName)
 	}
 
-	status := tc.ServerUpdateStatus{}
+	status := atscfg.ServerUpdateStatus{}
 	reqInf := toclientlib.ReqInf{}
 	err := torequtil.GetRetry(cl.NumRetries, "server_update_status_"+string(cacheHostName), &status, func(obj interface{}) error {
 		toStatus, toReqInf, err := cl.c.GetServerUpdateStatus(string(cacheHostName), *ReqOpts(reqHdr))
 		if err != nil {
 			return errors.New("getting server update status from Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "': " + err.Error())
 		}
-		status := obj.(*tc.ServerUpdateStatus)
+		status := obj.(*atscfg.ServerUpdateStatus)
 		if len(toStatus.Response) != 1 {
 			return errors.New("getting server update status from Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "': " + "expected 1 update_status for the server, got " + strconv.Itoa(len(toStatus.Response)))
 		}
-		*status = toStatus.Response[0]
+
+		*status = serverUpdateStatusesToLatest(toStatus.Response)[0]
 		reqInf = toReqInf
 		return nil
 	})
 	if err != nil {
-		return tc.ServerUpdateStatus{}, reqInf, errors.New("getting server update status: " + err.Error())
+		return atscfg.ServerUpdateStatus{}, reqInf, errors.New("getting server update status: " + err.Error())
 	}
 	return status, reqInf, nil
 }
 
 // SetServerUpdateStatus sets the server's update and reval statuses in Traffic Ops.
-func (cl *TOClient) SetServerUpdateStatus(cacheHostName tc.CacheName, updateStatus *bool, revalStatus *bool) (toclientlib.ReqInf, error) {
+func (cl *TOClient) SetServerUpdateStatus(cacheHostName tc.CacheName, configApply, revalApply *time.Time) (toclientlib.ReqInf, error) {
 	if cl.c == nil {
+		var updateStatus, revalStatus *bool
+		if configApply != nil {
+			*updateStatus = true
+			revalStatus = nil
+		}
+		if revalApply != nil {
+			*revalStatus = true
+			updateStatus = nil
+		}
+		return cl.old.SetServerUpdateStatus(cacheHostName, updateStatus, revalStatus)
+	}
+
+	reqInf := toclientlib.ReqInf{}
+	err := torequtil.GetRetry(cl.NumRetries, "set_server_update_status_"+string(cacheHostName), nil, func(obj interface{}) error {
+		_, toReqInf, err := cl.c.SetUpdateServerStatusTimes(string(cacheHostName), configApply, revalApply, *ReqOpts(nil))
+		if err != nil {
+			return errors.New("setting server update status in Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "': " + err.Error())
+		}
+		reqInf = toReqInf
+		return nil
+	})
+	if err != nil {
+		return reqInf, errors.New("getting server update status: " + err.Error())
+	}
+	return reqInf, nil
+}
+
+// SetServerUpdateStatusBoolCompat sets the server's update and reval statuses in Traffic Ops.
+// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+func (cl *TOClient) SetServerUpdateStatusBoolCompat(cacheHostName tc.CacheName, configApply, revalApply *time.Time, configApplyBool, revalApplyBool *bool) (toclientlib.ReqInf, error) {
+	if cl.c == nil {
+		var updateStatus, revalStatus *bool
+		if configApply != nil || configApplyBool != nil {
+			*updateStatus = true
+			revalStatus = nil
+		}
+		if revalApply != nil || revalApplyBool != nil {
+			*revalStatus = true
+			updateStatus = nil
+		}
 		return cl.old.SetServerUpdateStatus(cacheHostName, updateStatus, revalStatus)
 	}
 
 	reqInf := toclientlib.ReqInf{}
 	err := torequtil.GetRetry(cl.NumRetries, "set_server_update_status_"+string(cacheHostName), nil, func(obj interface{}) error {
-		_, toReqInf, err := cl.c.SetUpdateServerStatuses(string(cacheHostName), updateStatus, revalStatus, *ReqOpts(nil))
+		_, toReqInf, err := cl.SetServerUpdateStatusCompat(string(cacheHostName), configApply, revalApply, configApplyBool, revalApplyBool, *ReqOpts(nil))
 		if err != nil {
 			return errors.New("setting server update status in Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "': " + err.Error())
 		}
diff --git a/cache-config/t3cutil/toreq/conversions.go b/cache-config/t3cutil/toreq/conversions.go
index 94d870c8f..74c232493 100644
--- a/cache-config/t3cutil/toreq/conversions.go
+++ b/cache-config/t3cutil/toreq/conversions.go
@@ -22,6 +22,7 @@ package toreq
 import (
 	"errors"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 	"time"
@@ -49,6 +50,10 @@ func jobsToLatest(jobs []tc.InvalidationJobV4) []atscfg.InvalidationJob {
 	return atscfg.ToInvalidationJobs(jobs)
 }
 
+func serverUpdateStatusesToLatest(statuses []tc.ServerUpdateStatusV40) []atscfg.ServerUpdateStatus {
+	return atscfg.ToServerUpdateStatuses(statuses)
+}
+
 // GetJobsCompat gets jobs from any Traffic Ops built from the ATC `master` branch, and converts the different formats to the latest.
 // This makes t3c work with old or new Traffic Ops deployed from `master`,
 // though it doesn't make a version of t3c older than this work with a new TO,
@@ -145,3 +150,45 @@ func InvalidationJobV4FromLegacy(job InvalidationJobV4PlusLegacy) (tc.Invalidati
 
 	return tc.InvalidationJobV4(job.InvalidationJobV4ForLegacy), nil
 }
+
+// SetServerUpdateStatusCompat is a bridge to send both styles of query parameters to the
+// TO endpoint /servers/{hostname-or-id}/update. The current (old) is to send a bool
+// value, however this has resulted in an accidental race condition. The attempt to fix
+// this is to send a timestamp representing when the config or revalidation changes
+// have been applied.
+//
+// To ensure T3C is compatible with both the current releases and future releases
+// this function will send both "styles". Once both T3C and TO have been deployed
+// with the timestamp only V4 TO API endpoint, this function can be removed and the
+// V4 client function `SetUpdateServerStatusTimes` may be used instead (as intended).
+// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+func (cl *TOClient) SetServerUpdateStatusCompat(serverName string, configApplyTime, revalApplyTime *time.Time, configApplyBool, revalApplyBool *bool, opts toclient.RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+	reqInf := toclientlib.ReqInf{CacheHitStatus: toclientlib.CacheHitStatusMiss}
+	var alerts tc.Alerts
+
+	if opts.QueryParameters == nil {
+		opts.QueryParameters = url.Values{}
+	}
+
+	if configApplyTime != nil {
+		opts.QueryParameters.Set("config_apply_time", configApplyTime.Format(time.RFC3339Nano))
+	}
+
+	if revalApplyTime != nil {
+		opts.QueryParameters.Set("revalidate_apply_time", revalApplyTime.Format(time.RFC3339Nano))
+	}
+
+	if configApplyBool != nil {
+		opts.QueryParameters.Set("updated", "false")
+	}
+	if revalApplyBool != nil {
+		opts.QueryParameters.Set("reval_updated", "false")
+	}
+
+	path := `/servers/` + url.PathEscape(serverName) + `/update`
+	if len(opts.QueryParameters) > 0 {
+		path += "?" + opts.QueryParameters.Encode()
+	}
+	reqInf, err := cl.c.TOClient.Req(http.MethodPost, path, nil, opts.Header, &alerts)
+	return alerts, reqInf, err
+}
diff --git a/cache-config/t3cutil/toreq/toreqold/clientfuncs.go b/cache-config/t3cutil/toreq/toreqold/clientfuncs.go
index 13952387b..86c57a87f 100644
--- a/cache-config/t3cutil/toreq/toreqold/clientfuncs.go
+++ b/cache-config/t3cutil/toreq/toreqold/clientfuncs.go
@@ -547,21 +547,21 @@ func (cl *TOClient) GetStatuses() ([]tc.Status, toclientlib.ReqInf, error) {
 }
 
 // GetServerUpdateStatus returns the data, the Traffic Ops address, and any error.
-func (cl *TOClient) GetServerUpdateStatus(cacheHostName tc.CacheName) (tc.ServerUpdateStatus, toclientlib.ReqInf, error) {
-	status := tc.ServerUpdateStatus{}
+func (cl *TOClient) GetServerUpdateStatus(cacheHostName tc.CacheName) (atscfg.ServerUpdateStatus, toclientlib.ReqInf, error) {
+	status := atscfg.ServerUpdateStatus{}
 	reqInf := toclientlib.ReqInf{}
 	err := torequtil.GetRetry(cl.NumRetries, "server_update_status_"+string(cacheHostName), &status, func(obj interface{}) error {
 		toStatus, toReqInf, err := cl.c.GetServerUpdateStatus(string(cacheHostName))
 		if err != nil {
 			return errors.New("getting server update status from Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "': " + err.Error())
 		}
-		status := obj.(*tc.ServerUpdateStatus)
-		*status = toStatus
+		status := obj.(*atscfg.ServerUpdateStatus)
+		*status = serverUpdateStatusToLatest(&toStatus)
 		reqInf = toReqInf
 		return nil
 	})
 	if err != nil {
-		return tc.ServerUpdateStatus{}, reqInf, errors.New("getting server update status: " + err.Error())
+		return atscfg.ServerUpdateStatus{}, reqInf, errors.New("getting server update status: " + err.Error())
 	}
 	return status, reqInf, nil
 }
diff --git a/cache-config/t3cutil/toreq/toreqold/conversions.go b/cache-config/t3cutil/toreq/toreqold/conversions.go
index 28910ae99..79a37bc6d 100644
--- a/cache-config/t3cutil/toreq/toreqold/conversions.go
+++ b/cache-config/t3cutil/toreq/toreqold/conversions.go
@@ -56,3 +56,8 @@ func dsesToLatest(dses []tc.DeliveryServiceNullableV30) []atscfg.DeliveryService
 	}
 	return atscfg.ToDeliveryServices(newDSes)
 }
+
+func serverUpdateStatusToLatest(status *tc.ServerUpdateStatus) atscfg.ServerUpdateStatus {
+	upgraded := status.Upgrade()
+	return atscfg.ServerUpdateStatus(upgraded)
+}
diff --git a/docs/source/api/v2/servers.rst b/docs/source/api/v2/servers.rst
index eadf3b648..647f289c4 100644
--- a/docs/source/api/v2/servers.rst
+++ b/docs/source/api/v2/servers.rst
@@ -70,12 +70,12 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:        The integral, unique identifier of the CDN to which the server belongs
+:cdnName:      Name of the CDN to which the server belongs
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -124,7 +124,7 @@ Response Structure
 
 :type:       The name of the :term:`Type` of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (:term:`t3c`, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
@@ -243,8 +243,8 @@ Request Structure
 	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
 
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
-:xmppId:    A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
+:xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: An optional password used in XMPP communications with the server
 
 .. code-block:: http
@@ -294,12 +294,12 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:        The integral, unique identifier of the CDN to which the server belongs
+:cdnName:      Name of the CDN to which the server belongs
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -317,7 +317,7 @@ Response Structure
 :ip6IsService:   A boolean value which if ``true`` indicates that the IPv6 address will be used for routing content.
 :ip6Gateway:     The IPv6 address of the gateway used by ``interfaceName``
 :ipAddress:      The IPv4 address of ``interfaceName``
-:ipIsService:   A boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.
+:ipIsService:    A boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.
 :ipGateway:      The IPv4 address of the gateway used by ``interfaceName``
 :ipNetmask:      The IPv4 subnet mask used by ``interfaceName``
 :lastUpdated:    The date and time at which this server description was last modified
@@ -348,7 +348,7 @@ Response Structure
 
 :type:       The name of the 'type' of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
diff --git a/docs/source/api/v2/servers_id.rst b/docs/source/api/v2/servers_id.rst
index 92da230d8..fa96b0d9c 100644
--- a/docs/source/api/v2/servers_id.rst
+++ b/docs/source/api/v2/servers_id.rst
@@ -77,7 +77,7 @@ Request Structure
 	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
 
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: An optional password used in XMPP communications with the server
 
@@ -128,12 +128,12 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:        The integral, unique identifier of the CDN to which the server belongs
+:cdnName:      Name of the CDN to which the server belongs
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -182,7 +182,7 @@ Response Structure
 
 :type:       The name of the 'type' of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
diff --git a/docs/source/api/v3/servers.rst b/docs/source/api/v3/servers.rst
index 7c403e468..f27808314 100644
--- a/docs/source/api/v3/servers.rst
+++ b/docs/source/api/v3/servers.rst
@@ -153,7 +153,7 @@ Response Structure
 
 :type:       The name of the :term:`Type` of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (:term:`t3c`, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
@@ -296,7 +296,7 @@ Request Structure
 	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
 
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: An optional password used in XMPP communications with the server
 
@@ -365,12 +365,12 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:        The integral, unique identifier of the CDN to which the server belongs
+:cdnName:      Name of the CDN to which the server belongs
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -382,7 +382,7 @@ Response Structure
 :iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
 :iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
+:interfaces:     A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
 
 	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
 
@@ -438,7 +438,7 @@ Response Structure
 
 :type:       The name of the 'type' of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
diff --git a/docs/source/api/v3/servers_hostname_update.rst b/docs/source/api/v3/servers_hostname_update.rst
index 967556d6f..88ae99bb1 100644
--- a/docs/source/api/v3/servers_hostname_update.rst
+++ b/docs/source/api/v3/servers_hostname_update.rst
@@ -39,18 +39,22 @@ Request Structure
 
 .. table:: Request Query Parameters
 
-	+---------------+----------+--------------------------------------------------------------------------------------+
-	| Name          | Required | Description                                                                          |
-	+===============+==========+======================================================================================+
-	| updated       | no       | The value to set for the queue update flag on this server. May be 'true' or 'false'. |
-	+---------------+----------+--------------------------------------------------------------------------------------+
-	| reval_updated | no       | The value to set for the queue update flag on this server. May be 'true' or 'false'. |
-	+---------------+----------+--------------------------------------------------------------------------------------+
+	+----------------------------+----------+--------------------------------------------------------------------------------------+
+	| Name                       | Required | Description                                                                          |
+	+============================+==========+======================================================================================+
+	| updated (Deprecated)       | no       | The value to set for the queue update flag on this server. May be 'true' or 'false'. |
+	+----------------------------+----------+--------------------------------------------------------------------------------------+
+	| reval_updated (Deprecated) | no       | The value to set for the queue update flag on this server. May be 'true' or 'false'. |
+	+----------------------------+----------+--------------------------------------------------------------------------------------+
+
+	.. deprecated:: v3.1 Traffic Ops API
+
+		The boolean values above have resulted in an unintended race condition. These fields are removed in APIv4 and replaced.
 
 .. code-block:: http
 	:caption: Request Example
 
-	GET /api/3.0/servers/my-edge/update?updated=true&reval_updated=false HTTP/1.1
+	POST /api/3.0/servers/my-edge/update?updated=true&reval_updated=false HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
diff --git a/docs/source/api/v3/servers_id.rst b/docs/source/api/v3/servers_id.rst
index 60dd11a4f..6d160b3e2 100644
--- a/docs/source/api/v3/servers_id.rst
+++ b/docs/source/api/v3/servers_id.rst
@@ -93,7 +93,7 @@ Request Structure
 	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
 
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (:term:`t3c`, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: An optional password used in XMPP communications with the server
 
@@ -162,14 +162,14 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:        The integral, unique identifier of the CDN to which the server belongs
+:cdnName:      Name of the CDN to which the server belongs
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
 
-	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
+    .. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
 :hostName:       The (short) hostname of the server
 :httpsPort:      The port on which the server listens for incoming HTTPS connections/requests
@@ -179,7 +179,7 @@ Response Structure
 :iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
 :iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
+:interfaces:     A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
 
 	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
 
@@ -235,7 +235,7 @@ Response Structure
 
 :type:       The name of the 'type' of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
diff --git a/docs/source/api/v4/servers.rst b/docs/source/api/v4/servers.rst
index c68d5cb3d..548bdb80b 100644
--- a/docs/source/api/v4/servers.rst
+++ b/docs/source/api/v4/servers.rst
@@ -81,12 +81,14 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:        The integral, unique identifier of the CDN to which the server belongs
-:cdnName:      Name of the CDN to which the server belongs
-:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:         An identifier used to uniquely identify the server
+:cachegroup:       A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId:     An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:            The integral, unique identifier of the CDN to which the server belongs
+:cdnName:          Name of the CDN to which the server belongs
+:configUpdateTime: The last time an update was requested for this server. This field defaults to standard epoch
+:configApplyTime:  The last time an update was applied for this server. This field defaults to standard epoch
+:domainName:       The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:             An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -132,15 +134,17 @@ Response Structure
 	.. deprecated:: 3.0
 		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
 
-:offlineReason:  A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
-:physLocation:   The name of the physical location where the server resides
-:physLocationId: An integral, unique identifier for the physical location where the server resides
-:profile:        The :ref:`profile-name` of the :term:`Profile` used by this server
-:profileDesc:    A :ref:`profile-description` of the :term:`Profile` used by this server
-:profileId:      The :ref:`profile-id` the :term:`Profile` used by this server
-:revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
-:rack:           A string indicating "server rack" location
-:status:         The :term:`Status` of the server
+:offlineReason:   A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
+:physLocation:    The name of the physical location where the server resides
+:physLocationId:  An integral, unique identifier for the physical location where the server resides
+:profile:         The :ref:`profile-name` of the :term:`Profile` used by this server
+:profileDesc:     A :ref:`profile-description` of the :term:`Profile` used by this server
+:profileId:       The :ref:`profile-id` the :term:`Profile` used by this server
+:revalPending:    A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
+:revalUpdateTime: The last time a content invalidation/revalidation request was submitted for this server. This field defaults to standard epoch
+:revalApplyTime:  The last time a content invalidation/revalidation request was applied by this server. This field defaults to standard epoch
+:rack:            A string indicating "server rack" location
+:status:          The :term:`Status` of the server
 
 	.. seealso:: :ref:`health-proto`
 
@@ -154,7 +158,7 @@ Response Structure
 
 :type:       The name of the :term:`Type` of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (:term:`t3c`, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
@@ -175,6 +179,8 @@ Response Structure
 		"cachegroupId": 6,
 		"cdnId": 2,
 		"cdnName": "CDN-in-a-Box",
+		"configUpdateTime": "1969-12-31T17:00:00-07:00",
+		"configApplyTime": "1969-12-31T17:00:00-07:00",
 		"domainName": "infra.ciab.test",
 		"guid": null,
 		"hostName": "mid",
@@ -197,6 +203,8 @@ Response Structure
 		"profileId": 10,
 		"rack": "",
 		"revalPending": false,
+		"revalUpdateTime": "1969-12-31T17:00:00-07:00",
+		"revalApplyTime": "1969-12-31T17:00:00-07:00",
 		"status": "REPORTED",
 		"statusId": 3,
 		"tcpPort": 80,
@@ -287,7 +295,6 @@ Request Structure
 
 :physLocationId: An integral, unique identifier for the physical location where the server resides
 :profileId:      The :ref:`profile-id` the :term:`Profile` that shall be used by this server
-:revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
 :rack:           An optional string indicating "server rack" location
 :statusId:       The integral, unique identifier of the status of this server
 
@@ -298,7 +305,6 @@ Request Structure
 	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
 
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: An optional password used in XMPP communications with the server
 
@@ -361,29 +367,30 @@ Request Structure
 		"profileId": 10,
 		"statusId": 3,
 		"tcpPort": 80,
-		"typeId": 12,
-		"updPending": false
+		"typeId": 12
 	}
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:       A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId:     An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:            The integral, unique identifier of the CDN to which the server belongs
+:cdnName:          Name of the CDN to which the server belongs
+:configUpdateTime: The last time an update was requested for this server. This field defaults to standard epoch
+:configApplyTime:  The last time an update was applied for this server. This field defaults to standard epoch
+:domainName:       The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:             An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
-:hostName:       The (short) hostname of the server
-:httpsPort:      The port on which the server listens for incoming HTTPS connections/requests
-:id:             An integral, unique identifier for this server
-:iloIpAddress:   The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:iloIpGateway:   The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
-:iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:hostName:     The (short) hostname of the server
+:httpsPort:    The port on which the server listens for incoming HTTPS connections/requests
+:id:           An integral, unique identifier for this server
+:iloIpAddress: The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway: The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask: The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
+:iloUsername:  The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
 
 	:ipAddresses:       A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
@@ -418,15 +425,17 @@ Response Structure
 	.. deprecated:: 3.0
 		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
 
-:offlineReason:  A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
-:physLocation:   The name of the :term:`Physical Location` where the server resides
-:physLocationId: An integral, unique identifier for the :term:`Physical Location` where the server resides
-:profile:        The :ref:`profile-name` of the :term:`Profile` used by this server
-:profileDesc:    A :ref:`profile-description` of the :term:`Profile` used by this server
-:profileId:      The :ref:`profile-id` the :term:`Profile` used by this server
-:revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
-:rack:           A string indicating "server rack" location
-:status:         The status of the server
+:offlineReason:   A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
+:physLocation:    The name of the :term:`Physical Location` where the server resides
+:physLocationId:  An integral, unique identifier for the :term:`Physical Location` where the server resides
+:profile:         The :ref:`profile-name` of the :term:`Profile` used by this server
+:profileDesc:     A :ref:`profile-description` of the :term:`Profile` used by this server
+:profileId:       The :ref:`profile-id` the :term:`Profile` used by this server
+:revalPending:    A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
+:revalUpdateTime: The last time a content invalidation/revalidation request was submitted for this server. This field defaults to standard epoch
+:revalApplyTime:  The last time a content invalidation/revalidation request was applied by this server. This field defaults to standard epoch
+:rack:            A string indicating "server rack" location
+:status:          The status of the server
 
 	.. seealso:: :ref:`health-proto`
 
@@ -440,7 +449,7 @@ Response Structure
 
 :type:       The name of the 'type' of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
@@ -467,6 +476,8 @@ Response Structure
 		"cachegroupId": 6,
 		"cdnId": 2,
 		"cdnName": "CDN-in-a-Box",
+		"configUpdateTime": "1969-12-31T17:00:00-07:00",
+		"configApplyTime": "1969-12-31T17:00:00-07:00",
 		"domainName": "infra.ciab.test",
 		"guid": null,
 		"hostName": "test",
@@ -489,6 +500,8 @@ Response Structure
 		"profileId": 10,
 		"rack": null,
 		"revalPending": false,
+		"revalUpdateTime": "1969-12-31T17:00:00-07:00",
+		"revalApplyTime": "1969-12-31T17:00:00-07:00",
 		"status": "REPORTED",
 		"statusId": 3,
 		"tcpPort": 80,
diff --git a/docs/source/api/v4/servers_hostname_update.rst b/docs/source/api/v4/servers_hostname_update.rst
index c91193f36..f09964c40 100644
--- a/docs/source/api/v4/servers_hostname_update.rst
+++ b/docs/source/api/v4/servers_hostname_update.rst
@@ -40,18 +40,24 @@ Request Structure
 
 .. table:: Request Query Parameters
 
-	+---------------+----------+--------------------------------------------------------------------------------------+
-	| Name          | Required | Description                                                                          |
-	+===============+==========+======================================================================================+
-	| updated       | no       | The value to set for the queue update flag on this server. May be 'true' or 'false'. |
-	+---------------+----------+--------------------------------------------------------------------------------------+
-	| reval_updated | no       | The value to set for the queue update flag on this server. May be 'true' or 'false'. |
-	+---------------+----------+--------------------------------------------------------------------------------------+
+	+----------------------------+----------+--------------------------------------------------------------------------------------------------------------+
+	| Name                       | Required | Description                                                                                                  |
+	+============================+==========+==============================================================================================================+
+	| config_update_time         | no       | The value to set for when a queue update is requested for this server. Must be a valid RFC333Nano timestamp. |
+	+----------------------------+----------+--------------------------------------------------------------------------------------------------------------+
+	| config_apply_time          | no       | The value to set for when a queue update is applied for this server. Must be a valid RFC333Nano timestamp.   |
+	+----------------------------+----------+--------------------------------------------------------------------------------------------------------------+
+	| revalidate_update_time     | no       | The value to set for when a reval update is requested for this server. Must be a valid RFC333Nano timestamp. |
+	+----------------------------+----------+--------------------------------------------------------------------------------------------------------------+
+	| revalidate_apply_time      | no       | The value to set for when a reval update is applied for this server. Must be a valid RFC333Nano timestamp.   |
+	+----------------------------+----------+--------------------------------------------------------------------------------------------------------------+
+
+	.. note:: While none of the timestamps is required individually, at least one must be sent to the API.
 
 .. code-block:: http
 	:caption: Request Example
 
-	POST /api/4.0/servers/my-edge/update?updated=true&reval_updated=false HTTP/1.1
+	POST /api/4.0/servers/my-edge/update?config_update_time=2022-01-31T12%3A00%3A00.123456-07%3A00&revalidate_update_time=2022-01-31T12%3A00%3A00.123456-07%3A00 HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
@@ -81,7 +87,7 @@ Response Structure
 		"alerts" :
 			[
 				{
-					"text" : "successfully set server 'my-edge' updated=true reval_updated=false",
+					"text" : "successfully set server 'my-edge' config_update_time=2022-01-31T12:00:00.123456-07:00 revalidate_update_time=2022-01-31T12:00:00.123456-07:00",
 					"level" : "success"
 				}
 			]
diff --git a/docs/source/api/v4/servers_hostname_update_status.rst b/docs/source/api/v4/servers_hostname_update_status.rst
index 5aec43aa4..0ea35732b 100644
--- a/docs/source/api/v4/servers_hostname_update_status.rst
+++ b/docs/source/api/v4/servers_hostname_update_status.rst
@@ -56,11 +56,15 @@ Response Structure
 ------------------
 Each object in the returned array\ [#uniqueness]_ will contain the following fields:
 
+:configUpdateTime:     The last time an update was requested for this server. This field defaults to standard epoch
+:configApplyTime:      The last time an update was applied for this server. This field defaults to standard epoch
 :host_id:              The integral, unique identifier for the server for which the other fields in this object represent the pending updates and revalidation status
 :host_name:            The (short) hostname of the server for which the other fields in this object represent the pending updates and revalidation status
 :parent_pending:       A boolean telling whether or not any :term:`Topology` ancestor or :term:`parent` of this server has pending updates
 :parent_reval_pending: A boolean telling whether or not any :term:`Topology` ancestor or :term:`parent` of this server has pending :term:`Content Invalidation Jobs`
 :reval_pending:        ``true`` if the server has pending :term:`Content Invalidation Jobs`, ``false`` otherwise
+:revalUpdateTime:      The last time a content invalidation/revalidation request was submitted for this server. This field defaults to standard epoch
+:revalApplyTime:       The last time a content invalidation/revalidation request was applied by this server. This field defaults to standard epoch
 :status:               The name of the status of this server
 
 	.. seealso:: :ref:`health-proto` gives more information on how these statuses are used, and the ``GET`` method of the :ref:`to-api-statuses` endpoint can be used to retrieve information about all server statuses configured in Traffic Ops.
@@ -93,7 +97,11 @@ Each object in the returned array\ [#uniqueness]_ will contain the following fie
 		"host_id": 10,
 		"status": "REPORTED",
 		"parent_pending": false,
-		"parent_reval_pending": false
+		"parent_reval_pending": false,
+		"config_update_time": "2022-02-18T13:52:47.129174-07:00",
+		"config_apply_time": "2022-02-18T13:52:47.129174-07:00",
+		"revalidate_update_time": "2022-02-28T15:44:15.895145-07:00",
+		"revalidate_apply_time": "2022-02-18T13:52:47.129174-07:00"
 	}]}
 
 .. [#uniqueness] The returned object is an array, and there is no guarantee that one server exists for a given hostname. However, for each server in the array, that server's update status will be accurate for the server with that particular server ID.
diff --git a/docs/source/api/v4/servers_id.rst b/docs/source/api/v4/servers_id.rst
index 23ea9ed79..62176fbdd 100644
--- a/docs/source/api/v4/servers_id.rst
+++ b/docs/source/api/v4/servers_id.rst
@@ -81,11 +81,10 @@ Request Structure
 	.. deprecated:: 3.0
 		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
 
-:physLocationId: An integral, unique identifier for the physical location where the server resides
-:profileId:      The :ref:`profile-id` the :term:`Profile` that shall be used by this server
-:revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
-:rack:           An optional string indicating "server rack" location
-:statusId:       The integral, unique identifier of the status of this server
+:physLocationId:  An integral, unique identifier for the physical location where the server resides
+:profileId:       The :ref:`profile-id` the :term:`Profile` that shall be used by this server
+:rack:            An optional string indicating "server rack" location
+:statusId:        The integral, unique identifier of the status of this server
 
 	.. seealso:: :ref:`health-proto`
 
@@ -94,7 +93,6 @@ Request Structure
 	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
 
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: An optional password used in XMPP communications with the server
 
@@ -157,18 +155,19 @@ Request Structure
 		"profileId": 10,
 		"statusId": 3,
 		"tcpPort": 80,
-		"typeId": 12,
-		"updPending": false
+		"typeId": 12
 	}
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:       A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId:     An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:            The integral, unique identifier of the CDN to which the server belongs
+:configUpdateTime: The last time an update was requested for this server. This field defaults to standard epoch
+:configApplyTime:  The last time an update was applied for this server. This field defaults to standard epoch
+:cdnName:          Name of the CDN to which the server belongs
+:domainName:       The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:             An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -180,7 +179,7 @@ Response Structure
 :iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
 :iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
+:interfaces:     A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
 
 	:ipAddresses:       A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
 
@@ -214,15 +213,17 @@ Response Structure
 	.. deprecated:: 3.0
 		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
 
-:offlineReason:  A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
-:physLocation:   The name of the :term:`Physical Location` where the server resides
-:physLocationId: An integral, unique identifier for the :term:`Physical Location` where the server resides
-:profile:        The :ref:`profile-name` of the :term:`Profile` used by this server
-:profileDesc:    A :ref:`profile-description` of the :term:`Profile` used by this server
-:profileId:      The :ref:`profile-id` the :term:`Profile` used by this server
-:revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
-:rack:           A string indicating "server rack" location
-:status:         The status of the server
+:offlineReason:   A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
+:physLocation:    The name of the :term:`Physical Location` where the server resides
+:physLocationId:  An integral, unique identifier for the :term:`Physical Location` where the server resides
+:profile:         The :ref:`profile-name` of the :term:`Profile` used by this server
+:profileDesc:     A :ref:`profile-description` of the :term:`Profile` used by this server
+:profileId:       The :ref:`profile-id` the :term:`Profile` used by this server
+:revalPending:    A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
+:revalUpdateTime: The last time a content invalidation/revalidation request was submitted for this server. This field defaults to standard epoch
+:revalApplyTime:  The last time a content invalidation/revalidation request was applied by this server. This field defaults to standard epoch
+:rack:            A string indicating "server rack" location
+:status:          The status of the server
 
 	.. seealso:: :ref:`health-proto`
 
@@ -236,7 +237,7 @@ Response Structure
 
 :type:       The name of the 'type' of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Ops ORT
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (:term:`t3c`, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
@@ -267,6 +268,8 @@ Response Structure
 		"cachegroupId": 6,
 		"cdnId": 2,
 		"cdnName": "CDN-in-a-Box",
+		"configUpdateTime": "2022-02-28T15:44:15.895145-07:00",
+		"configApplyTime": "2022-02-18T13:52:47.129174-07:00",
 		"domainName": "infra.ciab.test",
 		"guid": null,
 		"hostName": "quest",
@@ -289,12 +292,14 @@ Response Structure
 		"profileId": 10,
 		"rack": null,
 		"revalPending": false,
+		"revalUpdateTime": "1969-12-31T17:00:00-07:00",
+		"revalApplyTime": "1969-12-31T17:00:00-07:00",
 		"status": "REPORTED",
 		"statusId": 3,
 		"tcpPort": 80,
 		"type": "MID",
 		"typeId": 12,
-		"updPending": false,
+		"updPending": true,
 		"xmppId": null,
 		"xmppPasswd": null,
 		"interfaces": [
@@ -354,12 +359,14 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belonged
-:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belonged
-:cdnId:        The integral, unique identifier of the CDN to which the server belonged
-:cdnName:      Name of the CDN to which the server belonged
-:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:         An identifier used to uniquely identify the server
+:cachegroup:       A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belonged
+:cachegroupId:     An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belonged
+:cdnId:            The integral, unique identifier of the CDN to which the server belonged
+:cdnName:          Name of the CDN to which the server belonged
+:configUpdateTime: The last time an update was requested for this server. This field defaults to standard epoch
+:configApplyTime:  The last time an update was applied for this server. This field defaults to standard epoch
+:domainName:       The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:             An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
@@ -405,15 +412,17 @@ Response Structure
 	.. deprecated:: 3.0
 		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
 
-:offlineReason:  A user-entered reason why the server was in ADMIN_DOWN or OFFLINE status
-:physLocation:   The name of the physical location where the server resided
-:physLocationId: An integral, unique identifier for the physical location where the server resided
-:profile:        The :ref:`profile-name` of the :term:`Profile` which was used by this server
-:profileDesc:    A :ref:`profile-description` of the :term:`Profile` which was used by this server
-:profileId:      The :ref:`profile-id` the :term:`Profile` which was used by this server
-:revalPending:   A boolean value which, if ``true`` indicates that this server had pending content invalidation/revalidation
-:rack:           A string indicating "server rack" location
-:status:         The :term:`Status` of the server
+:offlineReason:   A user-entered reason why the server was in ADMIN_DOWN or OFFLINE status
+:physLocation:    The name of the physical location where the server resided
+:physLocationId:  An integral, unique identifier for the physical location where the server resided
+:profile:         The :ref:`profile-name` of the :term:`Profile` which was used by this server
+:profileDesc:     A :ref:`profile-description` of the :term:`Profile` which was used by this server
+:profileId:       The :ref:`profile-id` the :term:`Profile` which was used by this server
+:revalPending:    A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
+:revalUpdateTime: The last time a content invalidation/revalidation request was submitted for this server. This field defaults to standard epoch
+:revalApplyTime:  The last time a content invalidation/revalidation request was applied by this server. This field defaults to standard epoch
+:rack:            A string indicating "server rack" location
+:status:          The :term:`Status` of the server
 
 	.. seealso:: :ref:`health-proto`
 
@@ -427,7 +436,7 @@ Response Structure
 
 :type:       The name of the :term:`Type` of this server
 :typeId:     The integral, unique identifier of the 'type' of this server
-:updPending: A boolean value which, if ``true``, indicates that the server had updates of some kind pending, typically to be acted upon by Traffic Ops :term:`ORT`
+:updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
 :xmppPasswd: The password used in XMPP communications with the server
 
@@ -454,6 +463,8 @@ Response Structure
 		"cachegroupId": 6,
 		"cdnId": 2,
 		"cdnName": "CDN-in-a-Box",
+		"configUpdateTime": "1969-12-31T17:00:00-07:00",
+		"configApplyTime": "1969-12-31T17:00:00-07:00",
 		"domainName": "infra.ciab.test",
 		"guid": null,
 		"hostName": "quest",
@@ -476,6 +487,8 @@ Response Structure
 		"profileId": 10,
 		"rack": null,
 		"revalPending": false,
+		"revalUpdateTime": "1969-12-31T17:00:00-07:00",
+		"revalApplyTime": "1969-12-31T17:00:00-07:00",
 		"status": "REPORTED",
 		"statusId": 3,
 		"tcpPort": 80,
diff --git a/lib/go-atscfg/atscfg.go b/lib/go-atscfg/atscfg.go
index 106410b3d..a81f25d0e 100644
--- a/lib/go-atscfg/atscfg.go
+++ b/lib/go-atscfg/atscfg.go
@@ -65,9 +65,14 @@ type DeliveryService tc.DeliveryServiceV40
 // but to only have to change it here, and the places where breaking symbol changes were made.
 type InvalidationJob tc.InvalidationJobV4
 
+// ServerUdpateStatus is a tc.ServerUdpateStatus for the latest lib/go-tc and traffic_ops/vx-client type.
+// This allows atscfg to not have to change the type everywhere it's used, every time ATC changes the base type,
+// but to only have to change it here, and the places where breaking symbol changes were made.
+type ServerUpdateStatus tc.ServerUpdateStatusV4
+
 // ToDeliveryServices converts a slice of the latest lib/go-tc and traffic_ops/vx-client type to the local alias.
 func ToDeliveryServices(dses []tc.DeliveryServiceV40) []DeliveryService {
-	ad := []DeliveryService{}
+	ad := make([]DeliveryService, 0, len(dses))
 	for _, ds := range dses {
 		ad = append(ad, DeliveryService(ds))
 	}
@@ -76,7 +81,7 @@ func ToDeliveryServices(dses []tc.DeliveryServiceV40) []DeliveryService {
 
 // V40ToDeliveryServices converts a slice of the old traffic_ops/client type to the local alias.
 func V40ToDeliveryServices(dses []tc.DeliveryServiceV40) []DeliveryService {
-	ad := []DeliveryService{}
+	ad := make([]DeliveryService, 0, len(dses))
 	for _, ds := range dses {
 		ad = append(ad, DeliveryService(ds))
 	}
@@ -85,7 +90,7 @@ func V40ToDeliveryServices(dses []tc.DeliveryServiceV40) []DeliveryService {
 
 // ToInvalidationJobs converts a slice of the latest lib/go-tc and traffic_ops/vx-client type to the local alias.
 func ToInvalidationJobs(jobs []tc.InvalidationJobV4) []InvalidationJob {
-	aj := []InvalidationJob{}
+	aj := make([]InvalidationJob, 0, len(jobs))
 	for _, job := range jobs {
 		aj = append(aj, InvalidationJob(job))
 	}
@@ -94,13 +99,22 @@ func ToInvalidationJobs(jobs []tc.InvalidationJobV4) []InvalidationJob {
 
 // ToServers converts a slice of the latest lib/go-tc and traffic_ops/vx-client type to the local alias.
 func ToServers(servers []tc.ServerV40) []Server {
-	as := []Server{}
+	as := make([]Server, 0, len(servers))
 	for _, sv := range servers {
 		as = append(as, Server(sv))
 	}
 	return as
 }
 
+// ToServerUpdateStatuses converts a slice of the latest lib/go-tc and traffic_ops/vx-client type to the local alias.
+func ToServerUpdateStatuses(statuses []tc.ServerUpdateStatusV40) []ServerUpdateStatus {
+	sus := make([]ServerUpdateStatus, 0, len(statuses))
+	for _, st := range statuses {
+		sus = append(sus, ServerUpdateStatus(st))
+	}
+	return sus
+}
+
 // CfgFile is all the information necessary to create an ATS config file, including the file name, path, data, and metadata.
 // This is provided as a convenience and unified structure for users. The lib/go-atscfg library doesn't actually use or return this. See ATSConfigFileData.
 type CfgFile struct {
diff --git a/lib/go-tc/cachegroups.go b/lib/go-tc/cachegroups.go
index 7910a7135..81b05ee94 100644
--- a/lib/go-tc/cachegroups.go
+++ b/lib/go-tc/cachegroups.go
@@ -19,9 +19,7 @@ package tc
  * under the License.
  */
 
-import (
-	"github.com/apache/trafficcontrol/lib/go-util"
-)
+import "github.com/apache/trafficcontrol/lib/go-util"
 
 // CacheGroupsResponse is a list of CacheGroups as a response.
 type CacheGroupsResponse struct {
diff --git a/lib/go-tc/servers.go b/lib/go-tc/servers.go
index edd2d5d63..cb5c3b702 100644
--- a/lib/go-tc/servers.go
+++ b/lib/go-tc/servers.go
@@ -962,6 +962,10 @@ type ServerV40 struct {
 	CommonServerProperties
 	Interfaces        []ServerInterfaceInfoV40 `json:"interfaces" db:"interfaces"`
 	StatusLastUpdated *time.Time               `json:"statusLastUpdated" db:"status_last_updated"`
+	ConfigUpdateTime  *time.Time               `json:"configUpdateTime" db:"config_update_time"`
+	ConfigApplyTime   *time.Time               `json:"configApplyTime" db:"config_apply_time"`
+	RevalUpdateTime   *time.Time               `json:"revalUpdateTime" db:"revalidate_update_time"`
+	RevalApplyTime    *time.Time               `json:"revalApplyTime" db:"revalidate_apply_time"`
 }
 
 // ServerV4 is the representation of a Server in the latest minor version of
@@ -1101,6 +1105,46 @@ func (s *ServerV40) ToServerV2FromV4() (ServerNullableV2, error) {
 	return legacyServer, nil
 }
 
+// ServerUpdateStatusV4 is the type of each entry in the `response` property of
+// the response from Traffic Ops to GET requests made to its
+// /servers/{{host name}}/update_status in the latest minor API
+// v4.0 endpoint.
+type ServerUpdateStatusV4 ServerUpdateStatusV40
+
+// ServerUpdateStatusV40 is the type of each entry in the `response` property of
+// the response from Traffic Ops to GET requests made to its
+// /servers/{{host name}}/update_status in API v4.0 endpoint.
+type ServerUpdateStatusV40 struct {
+	HostName             string     `json:"host_name"`
+	UpdatePending        bool       `json:"upd_pending"`
+	RevalPending         bool       `json:"reval_pending"`
+	UseRevalPending      bool       `json:"use_reval_pending"`
+	HostId               int        `json:"host_id"`
+	Status               string     `json:"status"`
+	ParentPending        bool       `json:"parent_pending"`
+	ParentRevalPending   bool       `json:"parent_reval_pending"`
+	ConfigUpdateTime     *time.Time `json:"config_update_time"`
+	ConfigApplyTime      *time.Time `json:"config_apply_time"`
+	RevalidateUpdateTime *time.Time `json:"revalidate_update_time"`
+	RevalidateApplyTime  *time.Time `json:"revalidate_apply_time"`
+}
+
+// Downgrade strips the Config and Revalidate timestamps from
+// ServerUpdateStatusV40 to return previous versions of the struct to ensure
+// previous compatibility.
+func (sus ServerUpdateStatusV40) Downgrade() ServerUpdateStatus {
+	return ServerUpdateStatus{
+		HostName:           sus.HostName,
+		UpdatePending:      sus.UpdatePending,
+		RevalPending:       sus.RevalPending,
+		UseRevalPending:    sus.UseRevalPending,
+		HostId:             sus.HostId,
+		Status:             sus.Status,
+		ParentPending:      sus.ParentPending,
+		ParentRevalPending: sus.ParentRevalPending,
+	}
+}
+
 // ServerUpdateStatus is the type of each entry in the `response` property of
 // the response from Traffic Ops to GET requests made to its
 // /servers/{{host name}}/update_status API endpoint.
@@ -1109,6 +1153,9 @@ func (s *ServerV40) ToServerV2FromV4() (ServerNullableV2, error) {
 // operations t3c has done/needs to do. For most purposes, using Server
 // structures will be better - especially since the basic principle of this
 // type is predicated on a lie: that server host names are unique.
+//
+// Deprecated: ServerUpdateStatus is for use only in APIs below V4. New code
+// should use ServerUpdateStatusV40 or newer.
 type ServerUpdateStatus struct {
 	HostName           string `json:"host_name"`
 	UpdatePending      bool   `json:"upd_pending"`
@@ -1120,11 +1167,26 @@ type ServerUpdateStatus struct {
 	ParentRevalPending bool   `json:"parent_reval_pending"`
 }
 
+// Upgrade converts the deprecated ServerUpdateStatus to a
+// ServerUpdateStatusV4 struct.
+func (sus ServerUpdateStatus) Upgrade() ServerUpdateStatusV4 {
+	return ServerUpdateStatusV4{
+		HostName:           sus.HostName,
+		UpdatePending:      sus.UpdatePending,
+		RevalPending:       sus.RevalPending,
+		UseRevalPending:    sus.UseRevalPending,
+		HostId:             sus.HostId,
+		Status:             sus.Status,
+		ParentPending:      sus.ParentPending,
+		ParentRevalPending: sus.ParentRevalPending,
+	}
+}
+
 // ServerUpdateStatusResponseV40 is the type of a response from the Traffic
 // Ops API to a request to its /servers/{{host name}}/update_status endpoint
 // in API version 4.0.
 type ServerUpdateStatusResponseV40 struct {
-	Response []ServerUpdateStatus `json:"response"`
+	Response []ServerUpdateStatusV40 `json:"response"`
 	Alerts
 }
 
diff --git a/traffic_ops/app/db/migrations/2022021523082100_server_config_update.down.sql b/traffic_ops/app/db/migrations/2022021523082100_server_config_update.down.sql
new file mode 100644
index 000000000..4ebde57b6
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022021523082100_server_config_update.down.sql
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+-- Add previously existing columns
+ALTER TABLE public.server 
+ADD COLUMN IF NOT EXISTS upd_pending bool NOT NULL DEFAULT false, 
+ADD COLUMN IF NOT EXISTS reval_pending bool NOT NULL DEFAULT false;
+
+-- Remove new columns
+ALTER TABLE public.server 
+DROP COLUMN IF EXISTS config_update_time, 
+DROP COLUMN IF EXISTS config_apply_time,
+DROP COLUMN IF EXISTS revalidate_update_time, 
+DROP COLUMN IF EXISTS revalidate_apply_time;
diff --git a/traffic_ops/app/db/migrations/2022021523082100_server_config_update.up.sql b/traffic_ops/app/db/migrations/2022021523082100_server_config_update.up.sql
new file mode 100644
index 000000000..e8e700084
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022021523082100_server_config_update.up.sql
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+-- Add columns, timestamp with time zone, nullable, default null
+ALTER TABLE public.server
+ADD COLUMN IF NOT EXISTS config_update_time TIMESTAMPTZ NOT NULL DEFAULT TIMESTAMP 'epoch',
+ADD COLUMN IF NOT EXISTS config_apply_time TIMESTAMPTZ NOT NULL DEFAULT TIMESTAMP 'epoch', 
+ADD COLUMN IF NOT EXISTS revalidate_update_time TIMESTAMPTZ NOT NULL DEFAULT TIMESTAMP 'epoch', 
+ADD COLUMN IF NOT EXISTS revalidate_apply_time TIMESTAMPTZ NOT NULL DEFAULT TIMESTAMP 'epoch';
+
+-- Drop previous columns
+ALTER TABLE public.server 
+DROP COLUMN IF EXISTS upd_pending, 
+DROP COLUMN IF EXISTS reval_pending;
diff --git a/traffic_ops/traffic_ops_golang/cachegroup/queueupdate.go b/traffic_ops/traffic_ops_golang/cachegroup/queueupdate.go
index 852718577..f46278ef1 100644
--- a/traffic_ops/traffic_ops_golang/cachegroup/queueupdate.go
+++ b/traffic_ops/traffic_ops_golang/cachegroup/queueupdate.go
@@ -20,9 +20,9 @@ package cachegroup
  */
 
 import (
-	"database/sql"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
@@ -32,6 +32,9 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 )
 
+const queue = "queue"
+const dequeue = "dequeue"
+
 func QueueUpdates(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
 	if userErr != nil || sysErr != nil {
@@ -40,12 +43,13 @@ func QueueUpdates(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
+	// Validate body and params
 	reqObj := tc.CachegroupQueueUpdatesRequest{}
 	if err := json.NewDecoder(r.Body).Decode(&reqObj); err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
 		return
 	}
-	if reqObj.Action != "queue" && reqObj.Action != "dequeue" {
+	if reqObj.Action != queue && reqObj.Action != dequeue {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("action must be 'queue' or 'dequeue'"), nil)
 		return
 	}
@@ -53,18 +57,37 @@ func QueueUpdates(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("cdn is a required field"), nil)
 		return
 	}
+
+	var cdnID int64
 	if reqObj.CDNID != nil && (reqObj.CDN == nil || *reqObj.CDN == "") {
-		cdn, ok, err := dbhelpers.GetCDNNameFromID(inf.Tx.Tx, int64(*reqObj.CDNID))
-		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting CDN name from ID '"+strconv.Itoa(int(*reqObj.CDNID))+"': "+err.Error()))
+		cdnName, ok, sysErr := dbhelpers.GetCDNNameFromID(inf.Tx.Tx, int64(*reqObj.CDNID))
+		if sysErr != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr)
 			return
 		}
 		if !ok {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("cdn "+strconv.Itoa(int(*reqObj.CDNID))+" does not exist"), nil)
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("cdn id %d does not exist", *reqObj.CDNID), nil)
 			return
 		}
-		reqObj.CDN = &cdn
+		cdnID = int64(*reqObj.CDNID)
+		reqObj.CDN = &cdnName
 	}
+
+	if reqObj.CDNID == nil && (reqObj.CDN != nil && *reqObj.CDN != "") {
+		var found bool
+		var cdnIDint int
+		cdnIDint, found, sysErr = dbhelpers.GetCDNIDFromName(inf.Tx.Tx, *reqObj.CDN)
+		cdnID = int64(cdnIDint)
+		if sysErr != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr)
+			return
+		}
+		if !found {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("cdn %s does not exist", *reqObj.CDN), nil)
+			return
+		}
+	}
+
 	cgID := inf.IntParams["id"]
 	cgName, ok, err := dbhelpers.GetCacheGroupNameFromID(inf.Tx.Tx, cgID)
 	if err != nil {
@@ -74,13 +97,21 @@ func QueueUpdates(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil)
 		return
 	}
+
+	// Verify rights
 	userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserHasCdnLock(inf.Tx.Tx, string(*reqObj.CDN), inf.User.UserName)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
 		return
 	}
-	queue := reqObj.Action == "queue"
-	updatedCaches, err := queueUpdates(inf.Tx.Tx, cgID, *reqObj.CDN, queue)
+
+	// Queue updates
+	var updatedCaches []tc.CacheName
+	if reqObj.Action == queue {
+		updatedCaches, err = dbhelpers.QueueUpdateForServerWithCachegroupCDN(inf.Tx.Tx, cgID, cdnID)
+	} else {
+		updatedCaches, err = dbhelpers.DequeueUpdateForServerWithCachegroupCDN(inf.Tx.Tx, cgID, cdnID)
+	}
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("queueing updates: "+err.Error()))
 		return
@@ -103,26 +134,3 @@ type QueueUpdatesResp struct {
 	CDN            tc.CDNName        `json:"cdn"`
 	CacheGroupID   int               `json:"cachegroupID"`
 }
-
-func queueUpdates(tx *sql.Tx, cgID int, cdn tc.CDNName, queue bool) ([]tc.CacheName, error) {
-	q := `
-UPDATE server SET upd_pending = $1
-WHERE server.cachegroup = $2
-AND server.cdn_id = (select id from cdn where name = $3)
-RETURNING server.host_name
-`
-	rows, err := tx.Query(q, queue, cgID, cdn)
-	if err != nil {
-		return nil, errors.New("querying queue updates: " + err.Error())
-	}
-	defer rows.Close()
-	names := []tc.CacheName{}
-	for rows.Next() {
-		name := ""
-		if err := rows.Scan(&name); err != nil {
-			return nil, errors.New("scanning queue updates: " + err.Error())
-		}
-		names = append(names, tc.CacheName(name))
-	}
-	return names, nil
-}
diff --git a/traffic_ops/traffic_ops_golang/cdn/queue.go b/traffic_ops/traffic_ops_golang/cdn/queue.go
index 5299d2bad..7c36a8ad1 100644
--- a/traffic_ops/traffic_ops_golang/cdn/queue.go
+++ b/traffic_ops/traffic_ops_golang/cdn/queue.go
@@ -50,9 +50,9 @@ func Queue(w http.ResponseWriter, r *http.Request) {
 	defer inf.Close()
 
 	cols := map[string]dbhelpers.WhereColumnInfo{
-		"cdnID":     {Column: "server.cdn_id", Checker: api.IsInt},
-		"typeID":    {Column: "server.type", Checker: nil},
-		"profileID": {Column: "server.profile", Checker: nil},
+		"cdnID":     {Column: "cdn_id", Checker: api.IsInt},
+		"typeID":    {Column: "type", Checker: nil},
+		"profileID": {Column: "profile", Checker: nil},
 	}
 
 	typeName := inf.Params["type"]
@@ -113,7 +113,8 @@ func Queue(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(params, cols)
+	// Ignore pagination to prevent possibility of not updating the entirity the requested CDN. Likewise, ignore orderby as nonessential.
+	where, _, _, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(params, cols)
 	if len(errs) > 0 {
 		errCode = http.StatusBadRequest
 		userErr = util.JoinErrs(errs)
@@ -121,10 +122,20 @@ func Queue(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	query := `UPDATE server SET upd_pending = :upd_pending`
-	query = query + where + orderBy + pagination
-	queryValues["upd_pending"] = reqObj.Action == "queue"
-	rowsAffected, err := queueUpdates(inf.Tx, queryValues, query)
+	query := ""
+	if reqObj.Action == "queue" {
+		query = `
+UPDATE public.server
+SET config_update_time = now()`
+		query = query + where
+	} else {
+		query = `
+UPDATE public.server
+SET config_update_time = config_apply_time`
+		query = query + where
+	}
+
+	rowsAffected, err := queueUpdates(inf.Tx, query, queryValues)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("queueing updates: %v", err))
 		return
@@ -135,7 +146,7 @@ func Queue(w http.ResponseWriter, r *http.Request) {
 }
 
 // queueUpdates is the helper function to queue/ dequeue updates on servers for a CDN, optionally filtered by type and/ or profile
-func queueUpdates(tx *sqlx.Tx, queryValues map[string]interface{}, query string) (int64, error) {
+func queueUpdates(tx *sqlx.Tx, query string, queryValues map[string]interface{}) (int64, error) {
 	result, err := tx.NamedExec(query, queryValues)
 	if err != nil {
 		return 0, errors.New("querying queue updates: " + err.Error())
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 09a1c9e18..175f4c57d 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -26,6 +26,7 @@ import (
 	"net/http"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
@@ -895,6 +896,7 @@ func CDNExists(cdnName string, tx *sql.Tx) (bool, error) {
 	return true, nil
 }
 
+// GetCDNNameFromID gets the CDN name from the given CDN ID.
 func GetCDNNameFromID(tx *sql.Tx, id int64) (tc.CDNName, bool, error) {
 	name := ""
 	if err := tx.QueryRow(`SELECT name FROM cdn WHERE id = $1`, id).Scan(&name); err != nil {
@@ -1080,7 +1082,7 @@ func GetServerIDFromName(serverName string, tx *sql.Tx) (int, bool, error) {
 	return id, true, nil
 }
 
-func GetServerNameFromID(tx *sql.Tx, id int) (string, bool, error) {
+func GetServerNameFromID(tx *sql.Tx, id int64) (string, bool, error) {
 	name := ""
 	if err := tx.QueryRow(`SELECT host_name FROM server WHERE id = $1`, id).Scan(&name); err != nil {
 		if err == sql.ErrNoRows {
@@ -1788,3 +1790,222 @@ func GetRegionNameFromID(tx *sql.Tx, regionID int) (string, bool, error) {
 	}
 	return regionName, true, nil
 }
+
+// QueueUpdateForServer sets the config update time for the server to now.
+func QueueUpdateForServer(tx *sql.Tx, serverID int64) error {
+	query := `
+UPDATE public.server
+SET config_update_time = now()
+WHERE server.id = $1;`
+
+	if _, err := tx.Exec(query, serverID); err != nil {
+		return fmt.Errorf("queueing config update for ServerID %d: %w", serverID, err)
+	}
+
+	return nil
+}
+
+// QueueUpdateForServerWithTime sets the config update time for the
+// server to the provided time.
+func QueueUpdateForServerWithTime(tx *sql.Tx, serverID int64, updateTime time.Time) error {
+	query := `
+UPDATE public.server
+SET config_update_time = $1
+WHERE server.id = $2;`
+
+	if _, err := tx.Exec(query, updateTime, serverID); err != nil {
+		return fmt.Errorf("queueing config update for ServerID %d with time %v: %w", serverID, updateTime, err)
+	}
+
+	return nil
+}
+
+// QueueUpdateForServerWithCachegroupCDN sets the config update time
+// for all servers belonging to the specified cachegroup (id) and cdn (id).
+func QueueUpdateForServerWithCachegroupCDN(tx *sql.Tx, cgID int, cdnID int64) ([]tc.CacheName, error) {
+	q := `
+UPDATE public.server
+SET config_update_time = now()
+WHERE server.cachegroup = $1 AND server.cdn_id = $2
+RETURNING server.host_name;`
+	rows, err := tx.Query(q, cgID, cdnID)
+	if err != nil {
+		return nil, fmt.Errorf("updating server config_update_time: %w", err)
+	}
+	defer log.Close(rows, "error closing rows for QueueUpdateForServerWithCachegroupCDN")
+	names := []tc.CacheName{}
+	for rows.Next() {
+		name := ""
+		if err := rows.Scan(&name); err != nil {
+			return nil, fmt.Errorf("scanning queue updates: %w", err)
+		}
+		names = append(names, tc.CacheName(name))
+	}
+	return names, nil
+}
+
+// QueueUpdateForServerWithTopologyCDN sets the config update time
+// for all servers within the specific topology (name) and cdn (id).
+func QueueUpdateForServerWithTopologyCDN(tx *sql.Tx, topologyName tc.TopologyName, cdnId int64) error {
+	query := `
+UPDATE public.server
+SET config_update_time = now()
+FROM public.cachegroup AS cg
+INNER JOIN public.topology_cachegroup AS tc ON tc.cachegroup = cg."name"
+WHERE cg.id = server.cachegroup
+AND tc.topology = $1
+AND server.cdn_id = $2;`
+	var err error
+	if _, err = tx.Exec(query, topologyName, cdnId); err != nil {
+		err = fmt.Errorf("queueing updates: %w", err)
+	}
+	return err
+}
+
+// DequeueUpdateForServer sets the config update time equal to the
+// config apply time, thereby effectively dequeueing any pending
+// updates for the server specified.
+func DequeueUpdateForServer(tx *sql.Tx, serverID int64) error {
+	query := `
+UPDATE public.server
+SET config_update_time = config_apply_time
+WHERE server.id = $1;`
+
+	if _, err := tx.Exec(query, serverID); err != nil {
+		return fmt.Errorf("applying config update for ServerID %d: %w", serverID, err)
+	}
+
+	return nil
+}
+
+// DequeueUpdateForServerWithCachegroupCDN sets the config update time equal to
+// the config apply time, thereby effectively dequeueing any pending
+// updates for the servers specified by the cachegroup (id) and the cdn (id).
+func DequeueUpdateForServerWithCachegroupCDN(tx *sql.Tx, cgID int, cdnID int64) ([]tc.CacheName, error) {
+	q := `
+UPDATE public.server
+SET config_update_time = config_apply_time
+WHERE server.cachegroup = $1
+AND server.cdn_id = $2
+RETURNING (SELECT s.host_name FROM "server" s WHERE s.id = server_id);`
+	rows, err := tx.Query(q, cgID, cdnID)
+	if err != nil {
+		return nil, fmt.Errorf("querying queue updates: %w", err)
+	}
+	defer log.Close(rows, "error closing rows for DequeueUpdateForServerWithCachegroupCDN")
+	names := []tc.CacheName{}
+	for rows.Next() {
+		name := ""
+		if err := rows.Scan(&name); err != nil {
+			return nil, fmt.Errorf("scanning queue updates: %w", err)
+		}
+		names = append(names, tc.CacheName(name))
+	}
+	return names, nil
+}
+
+// DequeueUpdateForServerWithTopologyCDN sets the config update time equal to
+// the config apply time, thereby effectively dequeueing any pending
+// updates for the servers specified by the topology (name) and the cdn (id).
+func DequeueUpdateForServerWithTopologyCDN(tx *sql.Tx, topologyName tc.TopologyName, cdnId int64) error {
+	query := `
+UPDATE public.server
+SET config_update_time = config_apply_time
+FROM cachegroup cg
+INNER JOIN topology_cachegroup tc ON tc.cachegroup = cg."name"
+WHERE cg.id = server.cachegroup
+AND tc.topology = $1
+AND server.cdn_id = $2;`
+	var err error
+	if _, err = tx.Exec(query, topologyName, cdnId); err != nil {
+		err = fmt.Errorf("queueing updates: %w", err)
+	}
+	return err
+}
+
+// SetApplyUpdateForServer sets the config apply time for the server to now.
+func SetApplyUpdateForServer(tx *sql.Tx, serverID int64) error {
+	query := `
+UPDATE public.server
+SET config_apply_time = now()
+WHERE server.id = $1;`
+
+	if _, err := tx.Exec(query, serverID); err != nil {
+		return fmt.Errorf("applying config update for ServerID %d: %w", serverID, err)
+	}
+
+	return nil
+}
+
+// SetApplyUpdateForServerWithTime sets the config apply time for the
+// server to the provided time.
+func SetApplyUpdateForServerWithTime(tx *sql.Tx, serverID int64, applyUpdateTime time.Time) error {
+	query := `
+UPDATE public.server
+SET config_apply_time = $1
+WHERE server.id = $2;`
+
+	if _, err := tx.Exec(query, applyUpdateTime, serverID); err != nil {
+		return fmt.Errorf("applying config update for ServerID %d with time %v: %w", serverID, applyUpdateTime, err)
+	}
+
+	return nil
+}
+
+// QueueRevalForServer sets the revalidate update time for the server to now.
+func QueueRevalForServer(tx *sql.Tx, serverID int64) error {
+	query := `
+UPDATE public.server
+SET revalidate_update_time = now()
+WHERE server.id = $1;`
+
+	if _, err := tx.Exec(query, serverID); err != nil {
+		return fmt.Errorf("queueing reval update for ServerID %d: %w", serverID, err)
+	}
+
+	return nil
+}
+
+// QueueRevalForServerWithTime sets the revalidate update time for the
+// server to the provided time.
+func QueueRevalForServerWithTime(tx *sql.Tx, serverID int64, revalTime time.Time) error {
+	query := `
+UPDATE public.server
+SET revalidate_update_time = $1
+WHERE server.id = $2;`
+
+	if _, err := tx.Exec(query, revalTime, serverID); err != nil {
+		return fmt.Errorf("queueing reval update for ServerID %d with time %v: %w", serverID, revalTime, err)
+	}
+
+	return nil
+}
+
+// SetApplyRevalForServer sets the revalidate apply time for the server to now.
+func SetApplyRevalForServer(tx *sql.Tx, serverID int64) error {
+	query := `
+UPDATE public.server
+SET revalidate_apply_time = now()
+WHERE server.id = $1;`
+
+	if _, err := tx.Exec(query, serverID); err != nil {
+		return fmt.Errorf("queueing reval update for ServerID %d: %w", serverID, err)
+	}
+
+	return nil
+}
+
+// SetApplyRevalForServerWithTime sets the revalidate apply time for the
+// server to the provided time.
+func SetApplyRevalForServerWithTime(tx *sql.Tx, serverID int64, applyRevalTime time.Time) error {
+	query := `
+UPDATE public.server
+SET revalidate_apply_time = $1
+WHERE server.id = $2;`
+
+	if _, err := tx.Exec(query, applyRevalTime, serverID); err != nil {
+		return fmt.Errorf("applying config update for ServerID %d with time %v: %w", serverID, applyRevalTime, err)
+	}
+
+	return nil
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go b/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go
index 921b4f3b0..4f7ab63ff 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go
@@ -167,7 +167,7 @@ s.status as status_id,
 s.tcp_port,
 t.name as server_type,
 s.type as server_type_id,
-s.upd_pending as upd_pending,
+s.config_update_time > s.config_apply_time AS upd_pending,
 ARRAY(select ssc.server_capability from server_server_capability ssc where ssc.server = s.id order by ssc.server_capability) as server_capabilities,
 ARRAY(select drc.required_capability from deliveryservices_required_capability drc where drc.deliveryservice_id = (select v from ds_id) order by drc.required_capability) as deliveryservice_capabilities
 `
@@ -244,7 +244,7 @@ ARRAY(select drc.required_capability from deliveryservices_required_capability d
 			}
 		}
 		if len(*s.ServerInterfaces) == 0 {
-			return nil, errors.New(fmt.Sprintf("no interfaces found on eligible server"))
+			return nil, fmt.Errorf("no interfaces found on eligible server. id: %d hostname: %s", *s.ID, *s.HostName)
 		}
 
 		eligible := true
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete.go
index b4fa6a58b..5d0275bd9 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete.go
@@ -173,7 +173,7 @@ func deleteDSS(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	serverName, exists, err := dbhelpers.GetServerNameFromID(tx, serverID)
+	serverName, exists, err := dbhelpers.GetServerNameFromID(tx, int64(serverID))
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server name from id: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
index f07e6a95d..985fcd31a 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
@@ -780,7 +780,7 @@ s.status as status_id,
 s.tcp_port,
 t.name as server_type,
 s.type as server_type_id,
-s.upd_pending as upd_pending
+s.config_update_time > s.config_apply_time AS upd_pending
 `
 
 	queryFormatString := `
diff --git a/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go b/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go
index 43b34e188..2f6a00ede 100644
--- a/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go
+++ b/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go
@@ -135,28 +135,29 @@ RETURNING
 	start_time as startTime
 `
 
-const revalQuery = `
-UPDATE server SET %s=TRUE
-WHERE server.status NOT IN (
-                             SELECT status.id
-                             FROM status
-                             WHERE name IN ('OFFLINE', 'PRE_PROD')
-                           )
+const queueUpdateOrRevalQuery = `
+UPDATE public.server
+SET %s = now()
+WHERE server.status IN (
+		SELECT status.id
+		FROM status
+		WHERE name IN ('ONLINE', 'REPORTED', 'ADMIN_DOWN')
+		)
      AND server.profile IN (
-                             SELECT profile_parameter.profile
-                             FROM profile_parameter
-                             WHERE profile_parameter.parameter IN (
-                                                                    SELECT parameter.id
-                                                                    FROM parameter
-                                                                    WHERE parameter.name='location'
-                                                                     AND parameter.config_file='regex_revalidate.config'
-                                                                  )
-                           )
+		SELECT profile_parameter.profile
+		FROM profile_parameter
+		WHERE profile_parameter.parameter IN (
+			SELECT parameter.id
+			FROM parameter
+			WHERE parameter.name='location'
+			AND parameter.config_file='regex_revalidate.config'
+			)
+		)
      AND server.cdn_id  =  (
-                             SELECT deliveryservice.cdn_id
-                             FROM deliveryservice
-                             WHERE deliveryservice.%s=$1
-                           )
+		SELECT deliveryservice.cdn_id
+		FROM deliveryservice
+		WHERE deliveryservice.%s=$1
+		);
 `
 
 const updateQuery = `
@@ -1528,19 +1529,19 @@ func setRevalFlags(d interface{}, tx *sql.Tx) error {
 		useReval = "0"
 	}
 
-	col := "reval_pending"
+	column := "revalidate_update_time"
 	if useReval == "0" {
-		col = "upd_pending"
+		column = "config_update_time"
 	}
 
 	var q string
 	switch t := d.(type) {
 	case uint:
-		q = fmt.Sprintf(revalQuery, col, "id")
+		q = fmt.Sprintf(queueUpdateOrRevalQuery, column, "id")
 	case string:
-		q = fmt.Sprintf(revalQuery, col, "xml_id")
+		q = fmt.Sprintf(queueUpdateOrRevalQuery, column, "xml_id")
 	default:
-		return fmt.Errorf("Invalid type passed to 'setRevalFlags': %v", t)
+		return fmt.Errorf("invalid type passed to 'setRevalFlags': %v", t)
 	}
 
 	row = tx.QueryRow(q, d)
diff --git a/traffic_ops/traffic_ops_golang/origin/origins.go b/traffic_ops/traffic_ops_golang/origin/origins.go
index 036b011a6..bfb152376 100644
--- a/traffic_ops/traffic_ops_golang/origin/origins.go
+++ b/traffic_ops/traffic_ops_golang/origin/origins.go
@@ -325,7 +325,6 @@ func (origin *TOOrigin) Update(h http.Header) (error, error, int) {
 		return userErr, sysErr, errCode
 	}
 
-	log.Debugf("about to run exec query: %s with origin: %++v", updateQuery(), origin)
 	resultRows, err := origin.ReqInfo.Tx.NamedQuery(updateQuery(), origin)
 	if err != nil {
 		return api.ParseDBError(err)
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index c51e11688..149db14d0 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -317,7 +317,7 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `servers/{id}/status$`, Handler: server.UpdateStatusHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "STATUS:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4766638513},
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPost, Path: `servers/{id}/queue_update$`, Handler: server.QueueUpdateHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:QUEUE", "SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41894713},
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `servers/{host_name}/update_status$`, Handler: server.GetServerUpdateStatusHandler, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4384515993},
-		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPost, Path: `servers/{id-or-name}/update$`, Handler: server.UpdateHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 443813233},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPost, Path: `servers/{id-or-name}/update$`, Handler: server.UpdateHandlerV4, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 443813233},
 
 		//Server: CRUD
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `servers/?$`, Handler: server.Read, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "DELIVERY-SERVICE:READ", "CDN:READ", "PHYSICAL-LOCATION:READ", "CACHE-GROUP:READ", "TYPE:READ", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 47209592853},
diff --git a/traffic_ops/traffic_ops_golang/server/put_status.go b/traffic_ops/traffic_ops_golang/server/put_status.go
index fc16ec087..80ba9b0b5 100644
--- a/traffic_ops/traffic_ops_golang/server/put_status.go
+++ b/traffic_ops/traffic_ops_golang/server/put_status.go
@@ -197,8 +197,8 @@ SELECT c.id
 FROM cachegroup c
 JOIN topology_descendants td ON c."name" = td.cachegroup
 )
-UPDATE server
-SET upd_pending = TRUE
+UPDATE public.server
+SET config_update_time = now()
 WHERE (server.cdn_id = $1
 	   AND server.cachegroup IN (
 			SELECT id
@@ -206,7 +206,7 @@ WHERE (server.cdn_id = $1
 			WHERE parent_cachegroup_id = $2
 				OR secondary_parent_cachegroup_id = $2
 			))
-		OR server.cachegroup IN (SELECT stc.id FROM server_topology_descendants stc)
+		OR server.cachegroup IN (SELECT stc.id FROM server_topology_descendants stc);
 `
 	if _, err := tx.Exec(q, cdnID, parentCachegroupID); err != nil {
 		return errors.New("queueing updates on child caches: " + err.Error())
diff --git a/traffic_ops/traffic_ops_golang/server/queue_update.go b/traffic_ops/traffic_ops_golang/server/queue_update.go
index c5327b3bc..e231ba2e7 100644
--- a/traffic_ops/traffic_ops_golang/server/queue_update.go
+++ b/traffic_ops/traffic_ops_golang/server/queue_update.go
@@ -20,20 +20,19 @@ package server
  */
 
 import (
-	"database/sql"
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 	"net/http"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 )
 
-// QueueUpdateHandler implements an http handler that updates a server's
-// upd_pending value.
+// QueueUpdateHandler implements an http handler that sets a server's
+// config update time value.
 func QueueUpdateHandler(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
 	if userErr != nil || sysErr != nil {
@@ -54,7 +53,6 @@ func QueueUpdateHandler(w http.ResponseWriter, r *http.Request) {
 	}
 
 	serverID := int64(inf.IntParams["id"])
-	queue := reqObj.Action == "queue"
 	cdnName, err := dbhelpers.GetCDNNameFromServerID(inf.Tx.Tx, serverID)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
@@ -65,15 +63,16 @@ func QueueUpdateHandler(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
 		return
 	}
-	ok, err := queueUpdate(inf.Tx.Tx, serverID, queue)
+
+	if reqObj.Action == "queue" {
+		err = dbhelpers.QueueUpdateForServer(inf.Tx.Tx, serverID)
+	} else {
+		err = dbhelpers.DequeueUpdateForServer(inf.Tx.Tx, serverID)
+	}
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("queueing updates: %v", err))
 		return
 	}
-	if !ok {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no server with id '%v' found", serverID), nil)
-		return
-	}
 
 	err = api.CreateChangeLogBuildMsg(
 		api.ApiChange,
@@ -94,18 +93,3 @@ func QueueUpdateHandler(w http.ResponseWriter, r *http.Request) {
 		Action:   reqObj.Action,
 	})
 }
-
-// queueUpdate sets the upd_pending column of a server to the value of queue. It
-// returns true if the identified server exists and was updated and false if no
-// server was updated either because it doesn't exist or there was an error.
-func queueUpdate(tx *sql.Tx, serverID int64, queue bool) (bool, error) {
-	const query = `UPDATE server SET upd_pending = $1 WHERE id = $2`
-
-	if result, err := tx.Exec(query, queue, serverID); err != nil {
-		return false, fmt.Errorf("updating server table: %v", err)
-	} else if rc, err := result.RowsAffected(); err != nil {
-		return false, fmt.Errorf("checking rows updated: %v", err)
-	} else {
-		return rc == 1, nil
-	}
-}
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index 81718ab8d..ad624a2a7 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -139,13 +139,17 @@ SELECT
 	p.description AS profile_desc,
 	s.profile AS profile_id,
 	s.rack,
-	s.reval_pending,
+	s.revalidate_update_time > s.revalidate_apply_time AS reval_pending,
+	s.revalidate_update_time,
+	s.revalidate_apply_time,
 	st.name AS status,
 	s.status AS status_id,
 	s.tcp_port,
 	t.name AS server_type,
 	s.type AS server_type_id,
-	s.upd_pending AS upd_pending,
+	s.config_update_time > s.config_apply_time AS upd_pending,
+	s.config_update_time,
+	s.config_apply_time,
 	s.xmpp_id,
 	s.xmpp_passwd,
 	s.status_last_updated
@@ -189,7 +193,6 @@ INSERT INTO server (
 	status,
 	tcp_port,
 	type,
-	upd_pending,
 	xmpp_id,
 	xmpp_passwd,
 	status_last_updated
@@ -214,43 +217,11 @@ INSERT INTO server (
 	:status_id,
 	:tcp_port,
 	:server_type_id,
-	:upd_pending,
 	:xmpp_id,
 	:xmpp_passwd,
 	:status_last_updated
 ) RETURNING
-	(SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS cachegroup,
-	cachegroup AS cachegroup_id,
-	cdn_id,
-	(SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
-	domain_name,
-	guid,
-	host_name,
-	https_port,
-	id,
-	ilo_ip_address,
-	ilo_ip_gateway,
-	ilo_ip_netmask,
-	ilo_password,
-	ilo_username,
-	last_updated,
-	mgmt_ip_address,
-	mgmt_ip_gateway,
-	mgmt_ip_netmask,
-	offline_reason,
-	(SELECT name FROM phys_location WHERE phys_location.id=server.phys_location) AS phys_location,
-	phys_location AS phys_location_id,
-	profile AS profile_id,
-	(SELECT description FROM profile WHERE profile.id=server.profile) AS profile_desc,
-	(SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
-	rack,
-	reval_pending,
-	(SELECT name FROM status WHERE status.id=server.status) AS status,
-	status AS status_id,
-	tcp_port,
-	(SELECT name FROM type WHERE type.id=server.type) AS server_type,
-	type AS server_type_id,
-	upd_pending
+	id
 `
 const insertQueryV4 = `
 INSERT INTO server (
@@ -274,7 +245,6 @@ INSERT INTO server (
 	status,
 	tcp_port,
 	type,
-	upd_pending,
 	xmpp_id,
 	xmpp_passwd
 ) VALUES (
@@ -298,42 +268,10 @@ INSERT INTO server (
 	:status_id,
 	:tcp_port,
 	:server_type_id,
-	:upd_pending,
 	:xmpp_id,
 	:xmpp_passwd
 ) RETURNING
-	(SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS cachegroup,
-	cachegroup AS cachegroup_id,
-	cdn_id,
-	(SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
-	domain_name,
-	guid,
-	host_name,
-	https_port,
-	id,
-	ilo_ip_address,
-	ilo_ip_gateway,
-	ilo_ip_netmask,
-	ilo_password,
-	ilo_username,
-	last_updated,
-	mgmt_ip_address,
-	mgmt_ip_gateway,
-	mgmt_ip_netmask,
-	offline_reason,
-	(SELECT name FROM phys_location WHERE phys_location.id=server.phys_location) AS phys_location,
-	phys_location AS phys_location_id,
-	profile AS profile_id,
-	(SELECT description FROM profile WHERE profile.id=server.profile) AS profile_desc,
-	(SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
-	rack,
-	reval_pending,
-	(SELECT name FROM status WHERE status.id=server.status) AS status,
-	status AS status_id,
-	tcp_port,
-	(SELECT name FROM type WHERE type.id=server.type) AS server_type,
-	type AS server_type_id,
-	upd_pending
+	id
 `
 
 const insertQuery = `
@@ -358,7 +296,6 @@ INSERT INTO server (
 	status,
 	tcp_port,
 	type,
-	upd_pending,
 	xmpp_id,
 	xmpp_passwd
 ) VALUES (
@@ -382,42 +319,10 @@ INSERT INTO server (
 	:status_id,
 	:tcp_port,
 	:server_type_id,
-	:upd_pending,
 	:xmpp_id,
 	:xmpp_passwd
 ) RETURNING
-	(SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS cachegroup,
-	cachegroup AS cachegroup_id,
-	cdn_id,
-	(SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
-	domain_name,
-	guid,
-	host_name,
-	https_port,
-	id,
-	ilo_ip_address,
-	ilo_ip_gateway,
-	ilo_ip_netmask,
-	ilo_password,
-	ilo_username,
-	last_updated,
-	mgmt_ip_address,
-	mgmt_ip_gateway,
-	mgmt_ip_netmask,
-	offline_reason,
-	(SELECT name FROM phys_location WHERE phys_location.id=server.phys_location) AS phys_location,
-	phys_location AS phys_location_id,
-	profile AS profile_id,
-	(SELECT description FROM profile WHERE profile.id=server.profile) AS profile_desc,
-	(SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
-	rack,
-	reval_pending,
-	(SELECT name FROM status WHERE status.id=server.status) AS status,
-	status AS status_id,
-	tcp_port,
-	(SELECT name FROM type WHERE type.id=server.type) AS server_type,
-	type AS server_type_id,
-	upd_pending
+	id
 `
 
 const updateQuery = `
@@ -442,44 +347,11 @@ UPDATE server SET
 	status=:status_id,
 	tcp_port=:tcp_port,
 	type=:server_type_id,
-	upd_pending=:upd_pending,
 	xmpp_passwd=:xmpp_passwd,
 	status_last_updated=:status_last_updated
 WHERE id=:id
 RETURNING
-	(SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS cachegroup,
-	cachegroup AS cachegroup_id,
-	cdn_id,
-	(SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
-	domain_name,
-	guid,
-	host_name,
-	https_port,
-	id,
-	ilo_ip_address,
-	ilo_ip_gateway,
-	ilo_ip_netmask,
-	ilo_password,
-	ilo_username,
-	last_updated,
-	mgmt_ip_address,
-	mgmt_ip_gateway,
-	mgmt_ip_netmask,
-	offline_reason,
-	(SELECT name FROM phys_location WHERE phys_location.id=server.phys_location) AS phys_location,
-	phys_location AS phys_location_id,
-	profile AS profile_id,
-	(SELECT description FROM profile WHERE profile.id=server.profile) AS profile_desc,
-	(SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
-	rack,
-	reval_pending,
-	(SELECT name FROM status WHERE status.id=server.status) AS status,
-	status AS status_id,
-	tcp_port,
-	(SELECT name FROM type WHERE type.id=server.type) AS server_type,
-	type AS server_type_id,
-	upd_pending,
-	status_last_updated
+	id
 `
 
 const originServerQuery = `
@@ -510,7 +382,6 @@ func validateCommon(s *tc.CommonServerProperties, tx *sql.Tx) []error {
 		"profileId":      validation.Validate(s.ProfileID, validation.NotNil),
 		"statusId":       validation.Validate(s.StatusID, validation.NotNil),
 		"typeId":         validation.Validate(s.TypeID, validation.NotNil),
-		"updPending":     validation.Validate(s.UpdPending, validation.NotNil),
 		"httpsPort":      validation.Validate(s.HTTPSPort, validation.By(tovalidate.IsValidPortNumber)),
 		"tcpPort":        validation.Validate(s.TCPPort, validation.By(tovalidate.IsValidPortNumber)),
 	})
@@ -565,6 +436,7 @@ func validateV1(s *tc.ServerNullableV11, tx *sql.Tx) error {
 	if s.IP6Address != nil && *s.IP6Address != "" {
 		validateErrs["ip6Address"] = validation.Validate(s.IP6Address, validation.By(tovalidate.IsValidIPv6CIDROrAddress))
 	}
+
 	errs = append(errs, tovalidate.ToErrors(validateErrs)...)
 	errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...)
 
@@ -679,6 +551,7 @@ func validateV4(s *tc.ServerV40, tx *sql.Tx) (string, error) {
 	if !serviceAddrV6Found && !serviceAddrV4Found {
 		errs = append(errs, errors.New("a server must have at least one service address"))
 	}
+
 	if errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...); errs != nil {
 		return serviceInterface, util.JoinErrs(errs)
 	}
@@ -782,6 +655,7 @@ func validateV3(s *tc.ServerV30, tx *sql.Tx) (string, error) {
 	if !serviceAddrV6Found && !serviceAddrV4Found {
 		errs = append(errs, errors.New("a server must have at least one service address"))
 	}
+
 	if errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...); errs != nil {
 		return serviceInterface, util.JoinErrs(errs)
 	}
@@ -1574,50 +1448,49 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	rows, err := inf.Tx.NamedQuery(updateQuery, server)
-	if err != nil {
-		userErr, sysErr, errCode = api.ParseDBError(err)
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+	serverID, errCode, userErr, sysErr := updateServer(inf.Tx, server)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
-	defer rows.Close()
-
-	rowsAffected := 0
-	for rows.Next() {
-		if err := rows.StructScan(&server); err != nil {
-			api.HandleErr(w, r, tx, http.StatusNotFound, nil, fmt.Errorf("scanning lastUpdated from server insert: %v", err))
-			return
-		}
-		rowsAffected++
-	}
 
-	if rowsAffected < 1 {
-		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no server found with this id"), nil)
+	if userErr, sysErr, errCode = deleteInterfaces(id, tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
 	}
-	if rowsAffected > 1 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("update for server #%d affected too many rows (%d)", *server.ID, rowsAffected))
+
+	if userErr, sysErr, errCode = createInterfaces(id, server.Interfaces, tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
 	}
 
-	if userErr, sysErr, errCode = deleteInterfaces(id, tx); userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+	where := `WHERE s.id = $1`
+	selquery := selectQuery + where
+	var srvr tc.ServerV40
+	if err := inf.Tx.QueryRowx(selquery, serverID).StructScan(&srvr); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
 
-	if userErr, sysErr, errCode = createInterfaces(id, server.Interfaces, tx); userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+	serversInterfaces, err := dbhelpers.GetServersInterfaces([]int{*srvr.ID}, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
+	if interfacesMap, ok := serversInterfaces[*srvr.ID]; ok {
+		for _, intfc := range interfacesMap {
+			srvr.Interfaces = append(srvr.Interfaces, intfc)
+		}
+	}
 
 	if inf.Version.Major >= 3 {
 		if userErr, sysErr, errCode = updateStatusLastUpdatedTime(id, &statusLastUpdatedTime, tx); userErr != nil || sysErr != nil {
 			api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 			return
 		}
-		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", server)
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", srvr)
 	} else {
-		v2Server, err := server.ToServerV2FromV4()
+		v2Server, err := srvr.ToServerV2FromV4()
 		if err != nil {
 			sysErr = fmt.Errorf("converting valid v3 server to a v2 structure: %v", err)
 			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
@@ -1630,39 +1503,66 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: updated", *server.HostName, *server.DomainName, *server.ID)
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: updated", *srvr.HostName, *srvr.DomainName, *srvr.ID)
 	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
+func updateServer(tx *sqlx.Tx, server tc.ServerV40) (int64, int, error, error) {
+
+	rows, err := tx.NamedQuery(updateQuery, server)
+	if err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		return 0, errCode, userErr, sysErr
+	}
+	defer rows.Close()
+
+	var serverId int64
+	rowsAffected := 0
+	for rows.Next() {
+		if err := rows.Scan(&serverId); err != nil {
+			return 0, http.StatusNotFound, nil, fmt.Errorf("scanning lastUpdated from server insert: %v", err)
+		}
+		rowsAffected++
+	}
+
+	if rowsAffected < 1 {
+		return 0, http.StatusNotFound, fmt.Errorf("no server found with id %d", *server.ID), nil
+	}
+	if rowsAffected > 1 {
+		return 0, http.StatusInternalServerError, nil, fmt.Errorf("update for server #%d affected too many rows (%d)", *server.ID, rowsAffected)
+	}
+
+	return serverId, http.StatusOK, nil, nil
+}
+
 func createV2(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 	var server tc.ServerNullableV2
 
-	tx := inf.Tx.Tx
-
 	if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
 	if server.ID != nil {
 		var prevID int
-		err := tx.QueryRow("SELECT id from server where id = $1", server.ID).Scan(&prevID)
+		err := inf.Tx.Tx.QueryRow("SELECT id from server where id = $1", server.ID).Scan(&prevID)
 		if err != nil && err != sql.ErrNoRows {
-			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("checking if server with id %d exists", *server.ID))
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("checking if server with id %d exists", *server.ID))
 			return
 		}
 		if prevID != 0 {
-			api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("server with id %d already exists. Please do not provide an id", *server.ID), nil)
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("server with id %d already exists. Please do not provide an id", *server.ID), nil)
 			return
 		}
 	}
 
 	server.XMPPID = newUUID()
 
-	if err := validateV2(&server, tx); err != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+	if err := validateV2(&server, inf.Tx.Tx); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
+
 	if server.CDNName != nil {
 		userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, *server.CDNName, inf.User.UserName)
 		if userErr != nil || sysErr != nil {
@@ -1676,75 +1576,72 @@ func createV2(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	resultRows, err := inf.Tx.NamedQuery(insertQuery, server)
+
+	serverID, err := createServerV2(inf.Tx, server)
 	if err != nil {
 		userErr, sysErr, errCode := api.ParseDBError(err)
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
-	defer resultRows.Close()
 
-	rowsAffected := 0
-	for resultRows.Next() {
-		rowsAffected++
-		if err := resultRows.StructScan(&server.CommonServerProperties); err != nil {
-			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("server create scanning: %v", err))
-			return
-		}
-	}
-	if rowsAffected == 0 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("server create: no server was inserted, no id was returned"))
-		return
-	} else if rowsAffected > 1 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from server insert"))
+	ifaces, err := server.LegacyInterfaceDetails.ToInterfacesV4(*server.IPIsService, *server.IP6IsService, server.RouterHostName, server.RouterPortName)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
 
-	ifaces, err := server.LegacyInterfaceDetails.ToInterfacesV4(*server.IPIsService, *server.IP6IsService, server.RouterHostName, server.RouterPortName)
-	if err != nil {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+	if userErr, sysErr, errCode := createInterfaces(int(serverID), ifaces, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
 
-	if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, tx); err != nil {
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+	where := `WHERE s.id = $1`
+	selquery := selectQuery + where
+	var s4 tc.ServerV40
+	if err := inf.Tx.QueryRowx(selquery, serverID).StructScan(&s4); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
+	s4.Interfaces = ifaces
 
+	srvr, err := s4.ToServerV2FromV4()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
 	alerts := tc.CreateAlerts(tc.SuccessLevel, "server was created.")
-	api.WriteAlertsObj(w, r, http.StatusOK, alerts, server)
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, srvr)
 
-	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *server.HostName, *server.DomainName, *server.ID)
-	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *srvr.HostName, *srvr.DomainName, *srvr.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx)
 }
 
 func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 	var server tc.ServerV30
 
-	tx := inf.Tx.Tx
-
 	if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
 	if server.ID != nil {
 		var prevID int
-		err := tx.QueryRow("SELECT id from server where id = $1", server.ID).Scan(&prevID)
+		err := inf.Tx.Tx.QueryRow("SELECT id from server where id = $1", server.ID).Scan(&prevID)
 		if err != nil && err != sql.ErrNoRows {
-			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("checking if server with id %d exists", *server.ID))
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("checking if server with id %d exists", *server.ID))
 			return
 		}
 		if prevID != 0 {
-			api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("server with id %d already exists. Please do not provide an id", *server.ID), nil)
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("server with id %d already exists. Please do not provide an id", *server.ID), nil)
 			return
 		}
 	}
 
 	server.XMPPID = newUUID()
-	_, err := validateV3(&server, tx)
+
+	_, err := validateV3(&server, inf.Tx.Tx)
 	if err != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
@@ -1765,75 +1662,73 @@ func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	resultRows, err := inf.Tx.NamedQuery(insertQueryV3, server)
+	serverID, err := createServerV3(inf.Tx, server)
 	if err != nil {
 		userErr, sysErr, errCode := api.ParseDBError(err)
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
-	defer resultRows.Close()
 
-	rowsAffected := 0
-	for resultRows.Next() {
-		rowsAffected++
-		if err := resultRows.StructScan(&server.CommonServerProperties); err != nil {
-			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("server create scanning: %v", err))
-			return
-		}
-	}
-	if rowsAffected == 0 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("server create: no server was inserted, no id was returned"))
+	interfaces, err := tc.ToInterfacesV4(server.Interfaces, server.RouterHostName, server.RouterPortName)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
-	} else if rowsAffected > 1 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from server insert"))
+	}
+	userErr, sysErr, errCode := createInterfaces(int(serverID), interfaces, inf.Tx.Tx)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
 
-	interfaces, err := tc.ToInterfacesV4(server.Interfaces, server.RouterHostName, server.RouterPortName)
-	if err != nil {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+	where := `WHERE s.id = $1`
+	selquery := selectQuery + where
+	var s4 tc.ServerV40
+	if err := inf.Tx.QueryRowx(selquery, serverID).StructScan(&s4); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
-	userErr, sysErr, errCode := createInterfaces(*server.ID, interfaces, tx)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+
+	s4.Interfaces = interfaces
+
+	srvr, err := s4.ToServerV3FromV4()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
 
 	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
-	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, srvr)
 
-	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *server.HostName, *server.DomainName, *server.ID)
-	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *srvr.HostName, *srvr.DomainName, *srvr.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx)
 }
 
 func createV4(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 	var server tc.ServerV40
 
-	tx := inf.Tx.Tx
-
 	if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
+
 	if server.ID != nil {
 		var prevID int
-		err := tx.QueryRow("SELECT id from server where id = $1", server.ID).Scan(&prevID)
+		err := inf.Tx.Tx.QueryRow("SELECT id from server where id = $1", server.ID).Scan(&prevID)
 		if err != nil && err != sql.ErrNoRows {
-			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("checking if server with id %d exists", *server.ID))
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("checking if server with id %d exists", *server.ID))
 			return
 		}
 		if prevID != 0 {
-			api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("server with id %d already exists. Please do not provide an id", *server.ID), nil)
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("server with id %d already exists. Please do not provide an id", *server.ID), nil)
 			return
 		}
 	}
 
-	str := uuid.New().String()
-	server.XMPPID = &str
-	_, err := validateV4(&server, tx)
+	server.XMPPID = newUUID()
+
+	_, err := validateV4(&server, inf.Tx.Tx)
 	if err != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
 		return
 	}
 
@@ -1854,41 +1749,110 @@ func createV4(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	resultRows, err := inf.Tx.NamedQuery(insertQueryV4, server)
+	serverID, err := createServerV4(inf.Tx, server)
 	if err != nil {
 		userErr, sysErr, errCode := api.ParseDBError(err)
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	userErr, sysErr, errCode := createInterfaces(int(serverID), server.Interfaces, inf.Tx.Tx)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
-	defer resultRows.Close()
+
+	where := `WHERE s.id = $1`
+	selquery := selectQuery + where
+	var srvr tc.ServerV40
+	if err := inf.Tx.QueryRowx(selquery, serverID).StructScan(&srvr); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	// TODO: Use returned values from SQL insert to ensure inserted values match
+	srvr.Interfaces = server.Interfaces
+
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, srvr)
+
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *srvr.HostName, *srvr.DomainName, *srvr.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx)
+}
+
+func createServerV4(tx *sqlx.Tx, server tc.ServerV40) (int64, error) {
+	rows, err := tx.NamedQuery(insertQueryV4, server)
+	if err != nil {
+		return 0, err
+	}
+	defer log.Close(rows, "failed to close rows for createServerV4")
 
 	rowsAffected := 0
-	for resultRows.Next() {
+	var serverID int64
+	for rows.Next() {
 		rowsAffected++
-		if err := resultRows.StructScan(&server.CommonServerProperties); err != nil {
-			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("server create scanning: %v", err))
-			return
+		if err := rows.Scan(&serverID); err != nil {
+			return 0, err
 		}
 	}
+
 	if rowsAffected == 0 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("server create: no server was inserted, no id was returned"))
-		return
+		return 0, errors.New("server create: no server was inserted, no id was returned")
 	} else if rowsAffected > 1 {
-		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from server insert"))
-		return
+		return 0, fmt.Errorf("too many ids returned from server insert: %d", rowsAffected)
 	}
 
-	userErr, sysErr, errCode := createInterfaces(*server.ID, server.Interfaces, tx)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
-		return
+	return serverID, nil
+}
+
+func createServerV3(tx *sqlx.Tx, server tc.ServerV30) (int64, error) {
+	rows, err := tx.NamedQuery(insertQueryV3, server)
+	if err != nil {
+		return 0, err
 	}
+	defer log.Close(rows, "failed to close rows for createServerV3")
 
-	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
-	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+	rowsAffected := 0
+	var serverID int64
+	for rows.Next() {
+		rowsAffected++
+		if err := rows.Scan(&serverID); err != nil {
+			return 0, err
+		}
+	}
 
-	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *server.HostName, *server.DomainName, *server.ID)
-	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+	if rowsAffected == 0 {
+		return 0, errors.New("server create: no server was inserted, no id was returned")
+	} else if rowsAffected > 1 {
+		return 0, fmt.Errorf("too many ids returned from server insert: %d", rowsAffected)
+	}
+
+	return serverID, nil
+}
+
+func createServerV2(tx *sqlx.Tx, server tc.ServerNullableV2) (int64, error) {
+	rows, err := tx.NamedQuery(insertQuery, server)
+	if err != nil {
+		return 0, err
+	}
+	defer log.Close(rows, "failed to close rows for createServerV2")
+
+	rowsAffected := 0
+	var serverID int64
+	for rows.Next() {
+		rowsAffected++
+		if err := rows.Scan(&serverID); err != nil {
+			return 0, err
+		}
+	}
+
+	if rowsAffected == 0 {
+		return 0, errors.New("server create: no server was inserted, no id was returned")
+	} else if rowsAffected > 1 {
+		return 0, fmt.Errorf("too many ids returned from server insert: %d", rowsAffected)
+	}
+
+	return serverID, nil
 }
 
 // Create is the handler for POST requests to /servers.
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
index e11592223..ef56814e2 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
@@ -287,7 +287,7 @@ func (ssc *TOServerServerCapability) Create() (error, error, int) {
 	tx := ssc.APIInfo().Tx
 
 	// Check existence prior to checking type
-	_, exists, err := dbhelpers.GetServerNameFromID(tx.Tx, *ssc.ServerID)
+	_, exists, err := dbhelpers.GetServerNameFromID(tx.Tx, int64(*ssc.ServerID))
 	if err != nil {
 		return nil, err, http.StatusInternalServerError
 	}
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status.go b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
index 600900299..b39554757 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
@@ -39,21 +39,25 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	serverUpdateStatus, err := getServerUpdateStatus(inf.Tx.Tx, inf.Config, inf.Params["host_name"])
+	serverUpdateStatuses, err := getServerUpdateStatus(inf.Tx.Tx, inf.Config, inf.Params["host_name"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
 	}
 	if inf.Version == nil || inf.Version.Major < 4 {
-		api.WriteRespRaw(w, r, serverUpdateStatus)
+		var downgradedStatuses []tc.ServerUpdateStatus
+		for _, status := range serverUpdateStatuses {
+			downgradedStatuses = append(downgradedStatuses, status.Downgrade())
+		}
+		api.WriteRespRaw(w, r, downgradedStatuses)
 	} else {
-		api.WriteResp(w, r, serverUpdateStatus)
+		api.WriteResp(w, r, serverUpdateStatuses)
 	}
 }
 
-func getServerUpdateStatus(tx *sql.Tx, cfg *config.Config, hostName string) ([]tc.ServerUpdateStatus, error) {
+func getServerUpdateStatus(tx *sql.Tx, cfg *config.Config, hostName string) ([]tc.ServerUpdateStatusV40, error) {
 
-	updateStatuses := []tc.ServerUpdateStatus{}
+	updateStatuses := []tc.ServerUpdateStatusV40{}
 
 	selectQuery := `
 /* topology_ancestors finds the ancestor topology nodes of the topology node for
@@ -82,15 +86,26 @@ UNION ALL
  * ancestor topology node found by topology_ancestors.
  */
 ), server_topology_ancestors AS (
-SELECT s.id, s.cachegroup, s.cdn_id, s.upd_pending, s.reval_pending, s.status, ta.base_server_id
+SELECT s.id, 
+	s.cachegroup,
+	s.cdn_id,
+	s.config_update_time > s.config_apply_time AS upd_pending,
+	s.revalidate_update_time > s.revalidate_apply_time AS reval_pending,
+	s.status,
+	ta.base_server_id
 	FROM server s
 	JOIN cachegroup c ON s.cachegroup = c.id
 	JOIN topology_ancestors ta ON c."name" = ta.cachegroup
 	JOIN status ON status.id = s.status
 	WHERE status.name = ANY($1::TEXT[])
 ), parentservers AS (
-SELECT ps.id, ps.cachegroup, ps.cdn_id, ps.upd_pending, ps.reval_pending, ps.status
-		FROM server ps
+SELECT ps.id, 
+	ps.cachegroup,
+	ps.cdn_id,
+	ps.config_update_time > ps.config_apply_time AS upd_pending,
+	ps.revalidate_update_time > ps.revalidate_apply_time AS reval_pending,
+	ps.status
+	FROM server ps
 	LEFT JOIN status AS pstatus ON pstatus.id = ps.status
 	LEFT JOIN type t ON ps."type" = t.id
 	WHERE pstatus.name = ANY($1::TEXT[])
@@ -106,9 +121,9 @@ SELECT
 	s.id,
 	s.host_name,
 	type.name AS type,
-	(s.reval_pending::BOOLEAN) AS server_reval_pending,
+	s.revalidate_update_time > s.revalidate_apply_time AS server_reval_pending,
 	use_reval_pending.value,
-	s.upd_pending,
+	s.config_update_time > s.config_apply_time AS server_upd_pending,
 	status.name AS status,
 	/* True if the cachegroup parent or any ancestor topology node has pending updates. */
 	TRUE IN (
@@ -123,7 +138,11 @@ SELECT
 		WHERE sta.base_server_id = s.id
 		AND sta.cdn_id = s.cdn_id
 		UNION SELECT COALESCE(BOOL_OR(ps.reval_pending), FALSE)
-	) AS parent_reval_pending
+	) AS parent_reval_pending,
+	s.config_update_time,
+	s.config_apply_time,
+	s.revalidate_update_time,
+	s.revalidate_apply_time
 	FROM use_reval_pending,
 		 server s
 LEFT JOIN status ON s.status = status.id
@@ -132,7 +151,7 @@ LEFT JOIN type ON type.id = s.type
 LEFT JOIN parentservers ps ON ps.cachegroup = cg.parent_cachegroup_id
 	AND ps.cdn_id = s.cdn_id
 WHERE s.host_name = $5
-GROUP BY s.id, s.host_name, type.name, server_reval_pending, use_reval_pending.value, s.upd_pending, status.name
+GROUP BY s.id, s.host_name, type.name, server_reval_pending, use_reval_pending.value, server_upd_pending, status.name, config_update_time, config_apply_time, revalidate_update_time, revalidate_apply_time
 ORDER BY s.id
 `
 
@@ -146,9 +165,9 @@ ORDER BY s.id
 	defer log.Close(rows, "getServerUpdateStatus(): unable to close db connection")
 
 	for rows.Next() {
-		var us tc.ServerUpdateStatus
+		var us tc.ServerUpdateStatusV40
 		var serverType string
-		if err := rows.Scan(&us.HostId, &us.HostName, &serverType, &us.RevalPending, &us.UseRevalPending, &us.UpdatePending, &us.Status, &us.ParentPending, &us.ParentRevalPending); err != nil {
+		if err := rows.Scan(&us.HostId, &us.HostName, &serverType, &us.RevalPending, &us.UseRevalPending, &us.UpdatePending, &us.Status, &us.ParentPending, &us.ParentRevalPending, &us.ConfigUpdateTime, &us.ConfigApplyTime, &us.RevalidateUpdateTime, &us.RevalidateApplyTime); err != nil {
 			log.Errorf("could not scan server update status: %s\n", err)
 			return nil, tc.DBError
 		}
diff --git a/traffic_ops/traffic_ops_golang/server/update.go b/traffic_ops/traffic_ops_golang/server/update.go
index 54ae48d11..f7c1b3f84 100644
--- a/traffic_ops/traffic_ops_golang/server/update.go
+++ b/traffic_ops/traffic_ops_golang/server/update.go
@@ -22,9 +22,11 @@ package server
 import (
 	"database/sql"
 	"errors"
+	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -32,6 +34,8 @@ import (
 )
 
 // UpdateHandler implements an http handler that updates a server's upd_pending and reval_pending values.
+//
+// Deprecated: As of V4, prefer to use UpdateHandlerV4. This provides legacy compatibility with V3 and lower.
 func UpdateHandler(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id-or-name"}, nil)
 	if userErr != nil || sysErr != nil {
@@ -41,10 +45,10 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
 	defer inf.Close()
 
 	idOrName := inf.Params["id-or-name"]
-	id, err := strconv.Atoi(idOrName)
+	serverID, err := strconv.Atoi(idOrName)
 	hostName := ""
 	if err == nil {
-		name, ok, err := dbhelpers.GetServerNameFromID(inf.Tx.Tx, id)
+		name, ok, err := dbhelpers.GetServerNameFromID(inf.Tx.Tx, int64(serverID))
 		if err != nil {
 			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server name from id '"+idOrName+"': "+err.Error()))
 			return
@@ -53,7 +57,7 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 		hostName = name
-		cdnName, err := dbhelpers.GetCDNNameFromServerID(inf.Tx.Tx, int64(id))
+		cdnName, err := dbhelpers.GetCDNNameFromServerID(inf.Tx.Tx, int64(serverID))
 		if err != nil {
 			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 			return
@@ -65,7 +69,8 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
 		}
 	} else {
 		hostName = idOrName
-		serverID, ok, err := dbhelpers.GetServerIDFromName(hostName, inf.Tx.Tx)
+		var ok bool
+		serverID, ok, err = dbhelpers.GetServerIDFromName(hostName, inf.Tx.Tx)
 		if err != nil {
 			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server id from name '"+idOrName+"': "+err.Error()))
 			return
@@ -107,58 +112,264 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
 		return !strings.HasPrefix(strings.ToLower(s), "f")
 	}
 
-	updatedPtr := (*bool)(nil)
+	values := new(updateValues)
+
 	if hasUpdated {
 		updatedBool := strToBool(updated)
-		updatedPtr = &updatedBool
+		values.configUpdateBool = &updatedBool
 	}
-	revalUpdatedPtr := (*bool)(nil)
+
 	if hasRevalUpdated {
 		revalUpdatedBool := strToBool(revalUpdated)
-		revalUpdatedPtr = &revalUpdatedBool
+		values.revalUpdateBool = &revalUpdatedBool
 	}
 
-	if err := setUpdateStatuses(inf.Tx.Tx, hostName, updatedPtr, revalUpdatedPtr); err != nil {
+	if err := setUpdateStatuses(inf.Tx.Tx, int64(serverID), *values); err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("setting updated statuses: "+err.Error()))
 		return
 	}
 
 	respMsg := "successfully set server '" + hostName + "'"
 	if hasUpdated {
-		respMsg += " updated=" + strconv.FormatBool(strToBool(updated))
+		respMsg += " updated=" + updated
 	}
 	if hasRevalUpdated {
-		respMsg += " reval_updated=" + strconv.FormatBool(strToBool(revalUpdated))
+		respMsg += " reval_updated=" + revalUpdated
 	}
 
 	api.WriteAlerts(w, r, http.StatusOK, tc.CreateAlerts(tc.SuccessLevel, respMsg))
 }
 
-// setUpdateStatuses sets the upd_pending and reval_pending columns of a server.
-// If updatePending or revalPending is nil, that value is not changed.
-func setUpdateStatuses(tx *sql.Tx, hostName string, updatePending *bool, revalPending *bool) error {
-	if updatePending == nil && revalPending == nil {
-		return errors.New("either updatePending or revalPending must not be nil")
+type updateValues struct {
+	configUpdateBool *bool // Deprecated, prefer timestamps
+	revalUpdateBool  *bool // Deprecated, prefer timestamps
+	configUpdateTime *time.Time
+	configApplyTime  *time.Time
+	revalUpdateTime  *time.Time
+	revalApplyTime   *time.Time
+}
+
+func parseQueryParams(params map[string]string) (*updateValues, error) {
+	var paramValues updateValues
+
+	// Verify query string parameters
+	configUpdatedBoolParam, hasConfigUpdatedBoolParam := params["updated"]     // Deprecated, but still required for backwards compatibility
+	revalUpdatedBoolParam, hasRevalUpdatedBoolParam := params["reval_updated"] // Deprecated, but still required for backwards compatibility
+	configUpdateTimeParam, hasConfigUpdateTimeParam := params["config_update_time"]
+	revalidateUpdateTimeParam, hasRevalidateUpdateTimeParam := params["revalidate_update_time"]
+	configApplyTimeParam, hasConfigApplyTimeParam := params["config_apply_time"]
+	revalidateApplyTimeParam, hasRevalidateApplyTimeParam := params["revalidate_apply_time"]
+
+	if !hasConfigApplyTimeParam && !hasRevalidateApplyTimeParam &&
+		!hasConfigUpdateTimeParam && !hasRevalidateUpdateTimeParam &&
+		!hasConfigUpdatedBoolParam && !hasRevalUpdatedBoolParam {
+		return nil, errors.New("must pass at least one query parameter: 'config_apply_time', 'revalidate_apply_time', 'config_update_time', 'revalidate_update_time' (may also pass bool `update` `reval_updated`)")
+
+	}
+	// Prevent collision between booleans and timestamps
+	if (hasConfigUpdateTimeParam || hasConfigApplyTimeParam) && hasConfigUpdatedBoolParam {
+		return nil, errors.New("conflicting parameters. may not pass `updated` along with either `config_update_time` or `config_apply_time`")
+
+	}
+	if (hasRevalidateUpdateTimeParam || hasRevalidateApplyTimeParam) && hasRevalUpdatedBoolParam {
+		return nil, errors.New("conflicting parameters. may not pass `reval_updated` along with either `revalidate_update_time` or `revalidate_apply_time`")
+
+	}
+
+	// Validate and parse parameters before attempting to apply them (don't want to partially apply various status before an error)
+	// Timestamps
+	if hasConfigUpdateTimeParam {
+		configUpdateTime, err := time.Parse(time.RFC3339Nano, configUpdateTimeParam)
+		if err != nil {
+			return nil, errors.New("query parameter 'config_update_time' must be valid RFC3339Nano format")
+		}
+		paramValues.configUpdateTime = &configUpdateTime
 	}
-	qry := `UPDATE server SET `
-	updateStrs := []string{}
-	nextI := 1
-	qryVals := []interface{}{}
-	if updatePending != nil {
-		updateStrs = append(updateStrs, `upd_pending = $`+strconv.Itoa(nextI))
-		nextI++
-		qryVals = append(qryVals, *updatePending)
+
+	if hasRevalidateUpdateTimeParam {
+		revalUpdateTime, err := time.Parse(time.RFC3339Nano, revalidateUpdateTimeParam)
+		if err != nil {
+			return nil, errors.New("query parameter 'revalidate_update_time' must be valid RFC3339Nano format")
+		}
+		paramValues.revalUpdateTime = &revalUpdateTime
 	}
-	if revalPending != nil {
-		updateStrs = append(updateStrs, `reval_pending = $`+strconv.Itoa(nextI))
-		nextI++
-		qryVals = append(qryVals, *revalPending)
+
+	if hasConfigApplyTimeParam {
+		configApplyTime, err := time.Parse(time.RFC3339Nano, configApplyTimeParam)
+		if err != nil {
+			return nil, errors.New("query parameter 'config_apply_time' must be valid RFC3339Nano format")
+		}
+		paramValues.configApplyTime = &configApplyTime
 	}
-	qry += strings.Join(updateStrs, ", ") + ` WHERE host_name = $` + strconv.Itoa(nextI)
-	qryVals = append(qryVals, hostName)
 
-	if _, err := tx.Exec(qry, qryVals...); err != nil {
-		return errors.New("executing: " + err.Error())
+	if hasRevalidateApplyTimeParam {
+		revalApplyTime, err := time.Parse(time.RFC3339Nano, revalidateApplyTimeParam)
+		if err != nil {
+			return nil, errors.New("query parameter 'revalidate_apply_time' must be valid RFC3339Nano format")
+		}
+		paramValues.revalApplyTime = &revalApplyTime
+	}
+
+	// Booleans
+	configUpdatedBool := strings.ToLower(configUpdatedBoolParam)
+	revalUpdatedBool := strings.ToLower(revalUpdatedBoolParam)
+
+	if hasConfigUpdatedBoolParam && configUpdatedBool != `t` && configUpdatedBool != `true` && configUpdatedBool != `f` && configUpdatedBool != `false` {
+		return nil, errors.New("query parameter 'updated' must be 'true' or 'false'")
+	}
+	if hasRevalUpdatedBoolParam && revalUpdatedBool != `t` && revalUpdatedBool != `true` && revalUpdatedBool != `f` && revalUpdatedBool != `false` {
+		return nil, errors.New("query parameter 'reval_updated' must be 'true' or 'false'")
+	}
+
+	strToBool := func(s string) bool {
+		return strings.HasPrefix(s, "t")
+	}
+
+	if hasConfigUpdatedBoolParam {
+		updateBool := strToBool(configUpdatedBool)
+		paramValues.configUpdateBool = &updateBool
+	}
+
+	if hasRevalUpdatedBoolParam {
+		revalUpdatedBool := strToBool(revalUpdatedBool)
+		paramValues.revalUpdateBool = &revalUpdatedBool
+	}
+	return &paramValues, nil
+}
+
+// setUpdateStatuses set timestamps for config update/apply and revalidation
+// update/apply. If any value is nil, no changes occur
+func setUpdateStatuses(tx *sql.Tx, serverID int64, values updateValues) error {
+
+	if values.configUpdateTime != nil {
+		if err := dbhelpers.QueueUpdateForServerWithTime(tx, serverID, *values.configUpdateTime); err != nil {
+			return fmt.Errorf("setting config apply time: %w", err)
+		}
+	}
+
+	if values.revalUpdateTime != nil {
+		if err := dbhelpers.QueueRevalForServerWithTime(tx, serverID, *values.revalUpdateTime); err != nil {
+			return fmt.Errorf("setting reval apply time: %w", err)
+		}
+	}
+
+	if values.configApplyTime != nil {
+		if err := dbhelpers.SetApplyUpdateForServerWithTime(tx, serverID, *values.configApplyTime); err != nil {
+			return fmt.Errorf("setting config apply time: %w", err)
+		}
+	}
+
+	if values.revalApplyTime != nil {
+		if err := dbhelpers.SetApplyRevalForServerWithTime(tx, serverID, *values.revalApplyTime); err != nil {
+			return fmt.Errorf("setting reval apply time: %w", err)
+		}
+	}
+
+	if values.configUpdateBool != nil {
+		if *values.configUpdateBool {
+			if err := dbhelpers.QueueUpdateForServer(tx, serverID); err != nil {
+				return err
+			}
+		} else {
+			if err := dbhelpers.SetApplyUpdateForServer(tx, serverID); err != nil {
+				return err
+			}
+		}
+	}
+
+	if values.revalUpdateBool != nil {
+		if *values.revalUpdateBool {
+			if err := dbhelpers.QueueRevalForServer(tx, serverID); err != nil {
+				return err
+			}
+		} else {
+			if err := dbhelpers.SetApplyRevalForServer(tx, serverID); err != nil {
+				return err
+			}
+		}
 	}
 	return nil
 }
+
+func responseMessage(idOrName string, values updateValues) string {
+	respMsg := "successfully set server '" + idOrName + "'"
+
+	if values.configUpdateBool != nil {
+		respMsg += " updated=" + strconv.FormatBool(*values.configUpdateBool)
+	}
+	if values.revalUpdateBool != nil {
+		respMsg += " reval_updated=" + strconv.FormatBool(*values.revalUpdateBool)
+	}
+
+	if values.configUpdateTime != nil {
+		respMsg += " config_update_time=" + (*values.configUpdateTime).Format(time.RFC3339Nano)
+	}
+	if values.revalUpdateTime != nil {
+		respMsg += " revalidate_update_time=" + (*values.revalUpdateTime).Format(time.RFC3339Nano)
+	}
+	if values.configApplyTime != nil {
+		respMsg += " config_apply_time=" + (*values.configApplyTime).Format(time.RFC3339Nano)
+	}
+	if values.revalApplyTime != nil {
+		respMsg += " revalidate_apply_time=" + (*values.revalApplyTime).Format(time.RFC3339Nano)
+	}
+
+	return respMsg
+}
+
+// UpdateHandler implements an http handler that updates a server's config update and reval times.
+func UpdateHandlerV4(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id-or-name"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	idOrName := inf.Params["id-or-name"]
+	serverID, err := strconv.ParseInt(idOrName, 10, 64)
+	if err != nil {
+		id, ok, err := dbhelpers.GetServerIDFromName(idOrName, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting server id from name '%v': %w", idOrName, err))
+			return
+		} else if !ok {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("server name '"+idOrName+"' not found"), nil)
+			return
+		}
+		serverID = int64(id)
+	}
+
+	cdnName, err := dbhelpers.GetCDNNameFromServerID(inf.Tx.Tx, serverID)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserHasCdnLock(inf.Tx.Tx, string(cdnName), inf.User.UserName)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
+		return
+	}
+
+	// TODO parse JSON body to trump Query Params?
+	updateValues, err := parseQueryParams(inf.Params)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	if updateValues == nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("updateValues should not be nil"))
+		return
+	}
+
+	err = setUpdateStatuses(inf.Tx.Tx, serverID, *updateValues)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	respMsg := responseMessage(idOrName, *updateValues)
+
+	api.WriteAlerts(w, r, http.StatusOK, tc.CreateAlerts(tc.SuccessLevel, respMsg))
+}
diff --git a/traffic_ops/traffic_ops_golang/servercheck/servercheck.go b/traffic_ops/traffic_ops_golang/servercheck/servercheck.go
index f243d6c54..ea49e8ddb 100644
--- a/traffic_ops/traffic_ops_golang/servercheck/servercheck.go
+++ b/traffic_ops/traffic_ops_golang/servercheck/servercheck.go
@@ -37,13 +37,13 @@ const ServerCheck_Get_Endpoint = "GET /servercheck"
 
 const serverInfoQuery = `
 SELECT server.host_name AS hostName,
-       server.id AS id,
-       profile.name AS profile,
-       status.name AS adminState,
-       cachegroup.name AS cacheGroup,
-       type.name AS type,
-       server.upd_pending AS updPending,
-       server.reval_pending AS revalPending
+	server.id AS id,
+	profile.name AS profile,
+	status.name AS adminState,
+	cachegroup.name AS cacheGroup,
+	type.name AS type,
+	server.config_update_time > server.config_apply_time AS upd_pending,
+	server.revalidate_update_time > server.revalidate_apply_time AS reval_pending
 FROM server
 LEFT JOIN profile ON server.profile = profile.id
 LEFT JOIN status ON server.status = status.id
@@ -153,7 +153,7 @@ func CreateUpdateServercheck(w http.ResponseWriter, r *http.Request) {
 
 func getServerID(id *int, hostname *string, tx *sql.Tx) (int, bool, error) {
 	if id != nil {
-		_, exists, err := dbhelpers.GetServerNameFromID(tx, *id)
+		_, exists, err := dbhelpers.GetServerNameFromID(tx, int64(*id))
 		return *id, exists, err
 	}
 	sID, exists, err := dbhelpers.GetServerIDFromName(*hostname, tx)
diff --git a/traffic_ops/traffic_ops_golang/topology/queue_update.go b/traffic_ops/traffic_ops_golang/topology/queue_update.go
index cf07be92c..c98a42136 100644
--- a/traffic_ops/traffic_ops_golang/topology/queue_update.go
+++ b/traffic_ops/traffic_ops_golang/topology/queue_update.go
@@ -91,29 +91,20 @@ func QueueUpdateHandler(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
 		return
 	}
-	if err := queueUpdates(inf.Tx.Tx, topologyName, reqObj.CDNID, reqObj.Action == "queue"); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Topology queueing updates: "+err.Error()))
-		return
+
+	if reqObj.Action == "queue" {
+		if err := dbhelpers.QueueUpdateForServerWithTopologyCDN(inf.Tx.Tx, topologyName, reqObj.CDNID); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("topology queueing updates: %w", err))
+			return
+		}
+	} else {
+		if err := dbhelpers.DequeueUpdateForServerWithTopologyCDN(inf.Tx.Tx, topologyName, reqObj.CDNID); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("topology queueing updates: %w", err))
+			return
+		}
 	}
 
 	message := fmt.Sprintf("TOPOLOGY: %s, ACTION: Topology server updates %sd", topologyName, reqObj.Action)
 	api.CreateChangeLogRawTx(api.ApiChange, message, inf.User, inf.Tx.Tx)
 	api.WriteResp(w, r, tc.TopologiesQueueUpdate{Action: reqObj.Action, CDNID: reqObj.CDNID, Topology: topologyName})
 }
-
-func queueUpdates(tx *sql.Tx, topologyName tc.TopologyName, cdnId int64, queue bool) error {
-	query := `
-UPDATE server s
-SET upd_pending = $1
-FROM cachegroup c, topology_cachegroup tc, cdn
-WHERE s.cachegroup = c.id
-AND c.name = tc.cachegroup
-AND tc.topology = $2
-AND s.cdn_id = $3
-`
-	var err error
-	if _, err = tx.Exec(query, queue, topologyName, cdnId); err != nil {
-		err = fmt.Errorf("queueing updates: %s", err)
-	}
-	return err
-}
diff --git a/traffic_ops/v2-client/server_update_status.go b/traffic_ops/v2-client/server_update_status.go
index 717f2d5b9..3aef33f74 100644
--- a/traffic_ops/v2-client/server_update_status.go
+++ b/traffic_ops/v2-client/server_update_status.go
@@ -82,7 +82,7 @@ func (to *Session) SetServerQueueUpdate(serverID int, queueUpdate bool) (tc.Serv
 	return resp, reqInf, err
 }
 
-// UpdateServerStatus updates a server's queue status and/or reval status.
+// SetUpdateServerStatuses updates a server's queue status and/or reval status.
 // Either updateStatus or revalStatus may be nil, in which case that status isn't updated (but not both, because that wouldn't do anything).
 func (to *Session) SetUpdateServerStatuses(serverName string, updateStatus *bool, revalStatus *bool) (ReqInf, error) {
 	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss}
diff --git a/traffic_ops/v3-client/server_update_status.go b/traffic_ops/v3-client/server_update_status.go
index 8159c5b85..18c8e4708 100644
--- a/traffic_ops/v3-client/server_update_status.go
+++ b/traffic_ops/v3-client/server_update_status.go
@@ -50,7 +50,7 @@ func (to *Session) SetServerQueueUpdate(serverID int, queueUpdate bool) (tc.Serv
 	return resp, reqInf, err
 }
 
-// UpdateServerStatus updates a server's queue status and/or reval status.
+// SetUpdateServerStatuses updates a server's queue status and/or reval status.
 // Either updateStatus or revalStatus may be nil, in which case that status isn't updated (but not both, because that wouldn't do anything).
 func (to *Session) SetUpdateServerStatuses(serverName string, updateStatus *bool, revalStatus *bool) (toclientlib.ReqInf, error) {
 	reqInf := toclientlib.ReqInf{CacheHitStatus: toclientlib.CacheHitStatusMiss}
diff --git a/traffic_ops/v4-client/server_update_status.go b/traffic_ops/v4-client/server_update_status.go
index e1b616559..b61223dd7 100644
--- a/traffic_ops/v4-client/server_update_status.go
+++ b/traffic_ops/v4-client/server_update_status.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
@@ -49,34 +50,29 @@ func (to *Session) SetServerQueueUpdate(serverID int, queueUpdate bool, opts Req
 	return resp, reqInf, err
 }
 
-// SetUpdateServerStatuses updates a server's queue status and/or reval status.
-// Either updateStatus or revalStatus may be nil, in which case that status
-// isn't updated (but not both, because that wouldn't do anything).
-func (to *Session) SetUpdateServerStatuses(serverName string, updateStatus *bool, revalStatus *bool, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+// SetUpdateServerStatusTimes updates a server's config queue status and/or reval status.
+// Each argument individually is optional, however at least one argument must not be nil.
+func (to *Session) SetUpdateServerStatusTimes(serverName string, configApplyTime, revalApplyTime *time.Time, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	reqInf := toclientlib.ReqInf{CacheHitStatus: toclientlib.CacheHitStatusMiss}
-	if updateStatus == nil && revalStatus == nil {
-		return tc.Alerts{}, reqInf, errors.New("either updateStatus or revalStatus must be non-nil; nothing to do")
+	var alerts tc.Alerts
+
+	if configApplyTime == nil && revalApplyTime == nil {
+		return alerts, reqInf, errors.New("one must be non-nil (configApplyTime, revalApplyTime); nothing to do")
 	}
 
 	if opts.QueryParameters == nil {
 		opts.QueryParameters = url.Values{}
 	}
 
-	if updateStatus != nil {
-		if *updateStatus {
-			opts.QueryParameters.Set("updated", "true")
-		} else {
-			opts.QueryParameters.Set("updated", "false")
-		}
+	if configApplyTime != nil {
+		cat := configApplyTime.Format(time.RFC3339Nano)
+		opts.QueryParameters.Set("config_apply_time", cat)
 	}
-	if revalStatus != nil {
-		if *revalStatus {
-			opts.QueryParameters.Set("reval_updated", "true")
-		} else {
-			opts.QueryParameters.Set("reval_updated", "false")
-		}
+	if revalApplyTime != nil {
+		rat := revalApplyTime.Format(time.RFC3339Nano)
+		opts.QueryParameters.Set("revalidate_apply_time", rat)
 	}
-	var alerts tc.Alerts
+
 	path := `/servers/` + url.PathEscape(serverName) + `/update`
 	reqInf, err := to.post(path, opts, nil, &alerts)
 	return alerts, reqInf, err


Test patch:
diff --git a/cache-config/testing/ort-tests/t3c-apply-diff_test.go b/cache-config/testing/ort-tests/t3c-apply-diff_test.go
index cdd916ac5..b35e3da37 100644
--- a/cache-config/testing/ort-tests/t3c-apply-diff_test.go
+++ b/cache-config/testing/ort-tests/t3c-apply-diff_test.go
@@ -54,8 +54,9 @@ func TestApplyDiff(t *testing.T) {
 			}
 			// queue and syncds to get changes
 
-			if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-				t.Fatalf("updating queue status failed: %v", err)
+			err = tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+			if err != nil {
+				t.Fatalf("failed to queue updates: %v", err)
 			}
 			out, code := t3cUpdateReload(DefaultCacheHostName, "syncds")
 			if code != 0 {
@@ -86,8 +87,9 @@ func TestApplyDiff(t *testing.T) {
 
 			// queue and syncds to get changes
 
-			if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-				t.Fatalf("updating queue status failed: %v", err)
+			err = tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+			if err != nil {
+				t.Fatalf("failed to queue updates: %v", err)
 			}
 			out, code := t3cUpdateReload(DefaultCacheHostName, "syncds")
 			if code != 0 {
diff --git a/cache-config/testing/ort-tests/t3c-apply-unset-update_test.go b/cache-config/testing/ort-tests/t3c-apply-unset-update_test.go
index a9915bbd4..0d3ae688f 100644
--- a/cache-config/testing/ort-tests/t3c-apply-unset-update_test.go
+++ b/cache-config/testing/ort-tests/t3c-apply-unset-update_test.go
@@ -24,7 +24,7 @@ import (
 
 	"github.com/apache/trafficcontrol/cache-config/t3cutil"
 	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
-	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 )
 
 func verifyUpdateStatusIsFalse() error {
@@ -32,7 +32,7 @@ func verifyUpdateStatusIsFalse() error {
 	if err != nil {
 		return fmt.Errorf("t3c-request failed: %w", err)
 	}
-	serverStatus := tc.ServerUpdateStatus{}
+	serverStatus := atscfg.ServerUpdateStatus{}
 	if err = json.Unmarshal([]byte(output), &serverStatus); err != nil {
 		return fmt.Errorf("failed to parse t3c-request output: %w", err)
 	}
@@ -53,7 +53,7 @@ func verifyUpdateStatusIsTrue() error {
 	if err != nil {
 		return fmt.Errorf("update-status run failed: %w", err)
 	}
-	serverStatus := tc.ServerUpdateStatus{}
+	serverStatus := atscfg.ServerUpdateStatus{}
 	if err = json.Unmarshal([]byte(output), &serverStatus); err != nil {
 		return fmt.Errorf("failed to parse update-status output: %w", err)
 	}
@@ -89,8 +89,9 @@ func TestT3cUnsetsUpdateFlag(t *testing.T) {
 		}
 
 		// set the update flag, so syncds will run
-		if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-			t.Fatalf("t3c-update failed: %v", err)
+		err := tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+		if err != nil {
+			t.Fatalf("failed to queue updates: %v", err)
 		}
 
 		if err := verifyUpdateStatusIsTrue(); err != nil {
diff --git a/cache-config/testing/ort-tests/t3c-apply-wait-for-parents_test.go b/cache-config/testing/ort-tests/t3c-apply-wait-for-parents_test.go
index 5f063c75d..94df96e26 100644
--- a/cache-config/testing/ort-tests/t3c-apply-wait-for-parents_test.go
+++ b/cache-config/testing/ort-tests/t3c-apply-wait-for-parents_test.go
@@ -26,6 +26,7 @@ import (
 
 	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
 	testutil "github.com/apache/trafficcontrol/cache-config/testing/ort-tests/util"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -60,13 +61,14 @@ func TestWaitForParentsTrue(t *testing.T) {
 
 		// queue on both child and parent
 
-		if err := ExecTOUpdater(childCacheHostName, false, true); err != nil {
+		err := tcd.QueueUpdatesForServer(childCacheHostName, true)
+		if err != nil {
 			t.Fatalf("queue updates on child failed: %v", err)
 		}
-		if err := ExecTOUpdater(parentCacheHostName, false, true); err != nil {
+		err = tcd.QueueUpdatesForServer(parentCacheHostName, true)
+		if err != nil {
 			t.Fatalf("queue updates on parent failed: %v", err)
 		}
-
 		// verify child has parent-pending in TO status endpoint.
 
 		if status, err := getUpdateStatus(childCacheHostName); err != nil {
@@ -102,15 +104,16 @@ func TestWaitForParentsTrue(t *testing.T) {
 			t.Errorf("expected: '%s' to still be queued after failed syncds run, actual: %+v", childCacheHostName, status)
 		}
 
-		if status, err := getUpdateStatus(parentCacheHostName); err != nil {
+		parentStatus, err := getUpdateStatus(parentCacheHostName)
+		if err != nil {
 			t.Fatalf("checking '%s' queue status: %v", parentCacheHostName, err)
-		} else if !status.UpdatePending {
-			t.Errorf("expected: '%s' to still be queued after unrelated child failed syncds run, actual: %+v", parentCacheHostName, status)
+		} else if !parentStatus.UpdatePending {
+			t.Errorf("expected: '%s' to still be queued after unrelated child failed syncds run, actual: %+v", parentCacheHostName, parentStatus)
 		}
 
 		// un-queue the parent
 
-		if err = ExecTOUpdater(parentCacheHostName, false, false); err != nil {
+		if err = ExecTOUpdater(parentCacheHostName, parentStatus.ConfigUpdateTime, parentStatus.RevalidateUpdateTime, util.BoolPtr(false), util.BoolPtr(false)); err != nil {
 			t.Fatalf("queue updates on child failed: %v", err)
 		}
 
@@ -135,7 +138,7 @@ func TestWaitForParentsTrue(t *testing.T) {
 		if status, err := getUpdateStatus(parentCacheHostName); err != nil {
 			t.Fatalf("checking '%s' queue status: %v", parentCacheHostName, err)
 		} else if status.UpdatePending {
-			t.Errorf("expected: '%s' to still be queued after unrelated child successful syncds run, actual: %+v", parentCacheHostName, status)
+			t.Errorf("expected: '%s' to not be queued after successful syncds wait-for-parents run, actual: %+v", parentCacheHostName, status)
 		}
 
 	})
@@ -169,13 +172,14 @@ func TestWaitForParentsDefaultReval(t *testing.T) {
 
 		// queue both child and parent
 
-		if err := ExecTOUpdater(childCacheHostName, false, true); err != nil {
+		err := tcd.QueueUpdatesForServer(childCacheHostName, true)
+		if err != nil {
 			t.Fatalf("queue updates on child failed: %v", err)
 		}
-		if err := ExecTOUpdater(parentCacheHostName, false, true); err != nil {
+		err = tcd.QueueUpdatesForServer(parentCacheHostName, true)
+		if err != nil {
 			t.Fatalf("queue updates on parent failed: %v", err)
 		}
-
 		// verify child has parent-pending in TO status endpoint.
 
 		if status, err := getUpdateStatus(childCacheHostName); err != nil {
@@ -237,10 +241,12 @@ func TestWaitForParentsFalse(t *testing.T) {
 
 		// queue both child and parent
 
-		if err := ExecTOUpdater(childCacheHostName, false, true); err != nil {
+		err := tcd.QueueUpdatesForServer(childCacheHostName, true)
+		if err != nil {
 			t.Fatalf("queue updates on child failed: %v", err)
 		}
-		if err := ExecTOUpdater(parentCacheHostName, false, true); err != nil {
+		err = tcd.QueueUpdatesForServer(parentCacheHostName, true)
+		if err != nil {
 			t.Fatalf("queue updates on parent failed: %v", err)
 		}
 
@@ -301,12 +307,12 @@ func TestWaitForParentsFalse(t *testing.T) {
 	})
 }
 
-func getUpdateStatus(hostName string) (tc.ServerUpdateStatus, error) {
-	st := tc.ServerUpdateStatus{}
+func getUpdateStatus(hostName string) (atscfg.ServerUpdateStatus, error) {
+	st := atscfg.ServerUpdateStatus{}
 	if output, err := runRequest(hostName, CMDUpdateStatus); err != nil {
-		return tc.ServerUpdateStatus{}, errors.New("t3c-request failed: " + err.Error())
+		return atscfg.ServerUpdateStatus{}, errors.New("t3c-request failed: " + err.Error())
 	} else if err = json.Unmarshal([]byte(output), &st); err != nil {
-		return tc.ServerUpdateStatus{}, errors.New("unmarshalling t3c-request json output: " + err.Error())
+		return atscfg.ServerUpdateStatus{}, errors.New("unmarshalling t3c-request json output: " + err.Error())
 	}
 	return st, nil
 }
diff --git a/cache-config/testing/ort-tests/t3c-reload_test.go b/cache-config/testing/ort-tests/t3c-reload_test.go
index d04107299..a9705b66c 100644
--- a/cache-config/testing/ort-tests/t3c-reload_test.go
+++ b/cache-config/testing/ort-tests/t3c-reload_test.go
@@ -31,7 +31,7 @@ func TestT3cReload(t *testing.T) {
 		tcdata.Profiles, tcdata.ProfileParameters, tcdata.Statuses,
 		tcdata.Divisions, tcdata.Regions, tcdata.PhysLocations,
 		tcdata.CacheGroups, tcdata.Servers, tcdata.Topologies,
-		tcdata.DeliveryServices}, func() {
+		tcdata.DeliveryServices, tcdata.InvalidationJobs}, func() {
 
 		t.Run("reload header rewrite", doTestT3cReloadHeaderRewrite)
 		t.Run("reload anything in trafficserver dir", doTestT3cReloadAnythingInTrafficserverDir)
@@ -53,8 +53,9 @@ func doTestT3cReloadHeaderRewrite(t *testing.T) {
 	}
 
 	// set the update flag, so syncds will run
-	if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-		t.Fatalf("t3c-update failed: %v", err)
+	err := tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+	if err != nil {
+		t.Fatalf("failed to set config update: %v", err)
 	}
 
 	stdOut, _ := t3cUpdateReload(DefaultCacheHostName, "syncds")
@@ -81,8 +82,9 @@ func doTestT3cReloadAnythingInTrafficserverDir(t *testing.T) {
 	}
 
 	// set the update flag, so syncds will run
-	if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-		t.Fatalf("t3c-update failed: %v", err)
+	err := tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+	if err != nil {
+		t.Fatalf("failed to set config update: %v", err)
 	}
 
 	stdOut, _ := t3cUpdateReload(DefaultCacheHostName, "syncds")
@@ -105,8 +107,9 @@ func doTestT3cReloadNoChange(t *testing.T) {
 	// no change, should not trigger a reload
 
 	// set the update flag, so syncds will run
-	if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-		t.Fatalf("t3c-update failed: %v", err)
+	err := tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+	if err != nil {
+		t.Fatalf("failed to set config update: %v", err)
 	}
 
 	stdOut, _ := t3cUpdateReload(DefaultCacheHostName, "syncds")
@@ -133,8 +136,10 @@ func doTestT3cRevalCallsReload(t *testing.T) {
 	}
 
 	// set the update flag, so reval will run
-	if err := ExecTOUpdater(DefaultCacheHostName, true, false); err != nil {
-		t.Fatalf("t3c-update failed: %v", err)
+	// TODO this sets the config update, do we need to do reval instead?
+	err := tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+	if err != nil {
+		t.Fatalf("failed to set config update: %v", err)
 	}
 
 	stdOut, _ := t3cUpdateReload(DefaultCacheHostName, "revalidate")
@@ -169,8 +174,10 @@ func doTestT3cReloadState(t *testing.T) {
 	}
 
 	// set the update flag, so syncds will run
-	if err := ExecTOUpdater(DefaultCacheHostName, false, true); err != nil {
-		t.Fatalf("t3c-update failed: %v", err)
+	// TODO this sets the config update, do we need to do reval instead?
+	err = tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
+	if err != nil {
+		t.Fatalf("failed to set config update: %v", err)
 	}
 
 	stdOut, _ := t3cUpdateReload(DefaultCacheHostName, "syncds")
diff --git a/cache-config/testing/ort-tests/t3c_mode_test.go b/cache-config/testing/ort-tests/t3c_mode_test.go
index d8cf4bc3c..12cb8d667 100644
--- a/cache-config/testing/ort-tests/t3c_mode_test.go
+++ b/cache-config/testing/ort-tests/t3c_mode_test.go
@@ -48,9 +48,9 @@ func verifyRemapConfigPlaced(t *testing.T) {
 		t.Fatalf("unable to remove %s: %v", remap, err)
 	}
 	// prepare for running syncds.
-	err = ExecTOUpdater(DefaultCacheHostName, false, true)
+	err = tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
 	if err != nil {
-		t.Fatalf("queue updates failed: %v", err)
+		t.Fatalf("failed to set config update: %v", err)
 	}
 
 	// remap.config is removed and atlanta-edge-03 should have
diff --git a/cache-config/testing/ort-tests/t3c_update_to_flags_test.go b/cache-config/testing/ort-tests/t3c_update_to_flags_test.go
index d855cea01..35ae74f98 100644
--- a/cache-config/testing/ort-tests/t3c_update_to_flags_test.go
+++ b/cache-config/testing/ort-tests/t3c_update_to_flags_test.go
@@ -19,7 +19,7 @@ import (
 	"testing"
 
 	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
-	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 )
 
 func TestT3cTOUpdates(t *testing.T) {
@@ -28,14 +28,14 @@ func TestT3cTOUpdates(t *testing.T) {
 		tcdata.Profiles, tcdata.ProfileParameters, tcdata.Statuses,
 		tcdata.Divisions, tcdata.Regions, tcdata.PhysLocations,
 		tcdata.CacheGroups, tcdata.Servers, tcdata.Topologies,
-		tcdata.DeliveryServices}, func() {
+		tcdata.DeliveryServices, tcdata.InvalidationJobs}, func() {
 
 		// retrieve the current server status
 		output, err := runRequest(DefaultCacheHostName, CMDUpdateStatus)
 		if err != nil {
 			t.Fatalf("update-status run failed: %v", err)
 		}
-		var serverStatus tc.ServerUpdateStatus
+		var serverStatus atscfg.ServerUpdateStatus
 		err = json.Unmarshal([]byte(output), &serverStatus)
 		if err != nil {
 			t.Fatalf("unmarshalling json output: %v", err)
@@ -43,17 +43,17 @@ func TestT3cTOUpdates(t *testing.T) {
 		if serverStatus.HostName != DefaultCacheHostName {
 			t.Fatalf("incorrect server status hostname, expected '%s', got '%s'", DefaultCacheHostName, serverStatus.HostName)
 		}
-		if serverStatus.RevalPending != false {
-			t.Fatal("expected RevalPending to be 'false'")
+		if serverStatus.RevalPending != true { // should be true since invalidation jobs were queued
+			t.Fatal("expected RevalPending to be 'true'")
 		}
 		if serverStatus.UpdatePending != false {
 			t.Fatal("expected UpdatePending to be 'false'")
 		}
 
 		// change the server update status
-		err = ExecTOUpdater(DefaultCacheHostName, true, true)
+		err = tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
 		if err != nil {
-			t.Fatalf("to_updater run failed: %v", err)
+			t.Fatalf("failed to set config update: %v", err)
 		}
 		// verify the update status is now 'true'
 		output, err = runRequest(DefaultCacheHostName, CMDUpdateStatus)
@@ -64,7 +64,7 @@ func TestT3cTOUpdates(t *testing.T) {
 		if err != nil {
 			t.Fatalf("failed to parse update-status run output: %v", err)
 		}
-		if serverStatus.RevalPending != true {
+		if serverStatus.RevalPending != true { // should be true since invalidation jobs were queued
 			t.Fatal("expected RevalPending to be 'true'")
 		}
 		if serverStatus.UpdatePending != true {
@@ -84,12 +84,17 @@ func TestT3cTOUpdates(t *testing.T) {
 		if err != nil {
 			t.Fatalf("failed to parse update-status run output: %v", err)
 		}
-		if serverStatus.RevalPending != true {
+		if serverStatus.RevalPending != true { // should be true since invalidation jobs were queued
 			t.Fatal("expected RevalPending to be 'true'")
 		}
 		if serverStatus.UpdatePending != false {
 			t.Fatal("expected UpdatePending to be 'false'")
 		}
+		if serverStatus.ConfigApplyTime != nil && serverStatus.ConfigUpdateTime != nil {
+			if !(*serverStatus.ConfigApplyTime).Equal(*serverStatus.ConfigUpdateTime) {
+				t.Fatalf("failed to set config apply time.\nExpc: %v\nRecv: %v", *serverStatus.ConfigUpdateTime, *serverStatus.ConfigApplyTime)
+			}
+		}
 
 		// run t3c revalidate and verify only the queue update flag is still 'false'
 		// and that the revalidate flag is now 'false'
@@ -111,5 +116,10 @@ func TestT3cTOUpdates(t *testing.T) {
 		if serverStatus.UpdatePending != false {
 			t.Error("expected UpdatePending to be 'false'")
 		}
+		if serverStatus.RevalidateApplyTime != nil && serverStatus.RevalidateUpdateTime != nil {
+			if !(*serverStatus.RevalidateApplyTime).Equal(*serverStatus.RevalidateUpdateTime) {
+				t.Fatalf("failed to set reval apply time.\nExpc: %v\nRecv: %v", *serverStatus.RevalidateUpdateTime, *serverStatus.RevalidateApplyTime)
+			}
+		}
 	})
 }
diff --git a/cache-config/testing/ort-tests/tc-fixtures.json b/cache-config/testing/ort-tests/tc-fixtures.json
index 4f7098aeb..a2ee412eb 100644
--- a/cache-config/testing/ort-tests/tc-fixtures.json
+++ b/cache-config/testing/ort-tests/tc-fixtures.json
@@ -1825,7 +1825,7 @@
           "lastUpdated": "2018-01-19T19:01:21.499423+00:00",
           "name": "trafficserver",
           "secure": true,
-          "value": "CHANGEME"
+          "value": "8.1.4-0.965df952e.el8.x86_64"
         },
         {
           "configFile": "empty-file.config",
@@ -2222,7 +2222,7 @@
           "lastUpdated": "2018-01-19T19:01:21.499423+00:00",
           "name": "trafficserver",
           "secure": true,
-          "value": "CHANGEME"
+          "value": "8.1.4-0.965df952e.el8.x86_64"
         },
         {
           "configFile": "empty-file.config",
diff --git a/cache-config/testing/ort-tests/tcdata/invalidation_jobs.go b/cache-config/testing/ort-tests/tcdata/invalidation_jobs.go
new file mode 100644
index 000000000..a2a9feef2
--- /dev/null
+++ b/cache-config/testing/ort-tests/tcdata/invalidation_jobs.go
@@ -0,0 +1,47 @@
+package tcdata
+
+import (
+	"net/http"
+	"testing"
+)
+
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+func (r *TCData) CreateTestInvalidationJobs(t *testing.T) {
+	// loop through invalidation jobs and create
+	for _, job := range r.TestData.InvalidationJobs {
+		resp, _, err := TOSession.CreateInvalidationJob(job)
+		t.Log("Response: ", job.Regex, " ", resp)
+		if err != nil {
+			t.Errorf("could not CREATE jobs: %v", err)
+		}
+	}
+}
+
+func (r *TCData) DeleteTestInvalidationJobs(t *testing.T) {
+	for _, job := range r.TestData.InvalidationJobs {
+
+		jobs, _, err := TOSession.GetInvalidationJobsWithHdr(job.DeliveryService, nil, http.Header{})
+		if err != nil {
+			t.Errorf("could not request jobs for DeliveryService: %v err: %v", job.DeliveryService, err)
+		}
+		for _, j := range jobs {
+			if _, _, err = TOSession.DeleteInvalidationJob(*j.ID); err != nil {
+				t.Errorf("failed to delete invalidation job %d", *j.ID)
+			}
+		}
+	}
+}
diff --git a/cache-config/testing/ort-tests/tcdata/servers.go b/cache-config/testing/ort-tests/tcdata/servers.go
index 6099ae6b1..ff1d779f3 100644
--- a/cache-config/testing/ort-tests/tcdata/servers.go
+++ b/cache-config/testing/ort-tests/tcdata/servers.go
@@ -16,6 +16,7 @@ package tcdata
 */
 
 import (
+	"fmt"
 	"net/url"
 	"testing"
 
@@ -141,3 +142,28 @@ func (r *TCData) DeleteTestServers(t *testing.T) {
 		}
 	}
 }
+
+func (r *TCData) QueueUpdatesForServer(hostname string, queue bool) error {
+	params := url.Values{}
+	params.Set("hostName", hostname)
+
+	resp, _, err := TOSession.GetServersWithHdr(&params, nil)
+	if err != nil {
+		return fmt.Errorf("cannot GET Server by hostname '%s': %v - %v", hostname, err, resp.Alerts)
+	}
+	if len(resp.Response) > 0 {
+		if len(resp.Response) > 1 {
+			return fmt.Errorf("expected exactly one server by hostname '%s' - actual: %d", hostname, len(resp.Response))
+		}
+		respServer := resp.Response[0]
+
+		if respServer.ID == nil {
+			return fmt.Errorf("server '%s' had nil ID", hostname)
+		}
+
+		if _, _, err := TOSession.SetServerQueueUpdate(*respServer.ID, queue); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/cache-config/testing/ort-tests/tcdata/session.go b/cache-config/testing/ort-tests/tcdata/session.go
index c55c0dccb..3d6d4bd78 100644
--- a/cache-config/testing/ort-tests/tcdata/session.go
+++ b/cache-config/testing/ort-tests/tcdata/session.go
@@ -18,7 +18,7 @@ package tcdata
 import (
 	"time"
 
-	"github.com/apache/trafficcontrol/traffic_ops/v3-client"
+	client "github.com/apache/trafficcontrol/traffic_ops/v3-client"
 
 	_ "github.com/lib/pq"
 )
diff --git a/cache-config/testing/ort-tests/tcdata/tcdata.go b/cache-config/testing/ort-tests/tcdata/tcdata.go
index 6428caff2..0c565ad79 100644
--- a/cache-config/testing/ort-tests/tcdata/tcdata.go
+++ b/cache-config/testing/ort-tests/tcdata/tcdata.go
@@ -53,6 +53,7 @@ type TrafficControl struct {
 	Divisions                                         []tc.Division                           `json:"divisions"`
 	Federations                                       []tc.CDNFederation                      `json:"federations"`
 	FederationResolvers                               []tc.FederationResolver                 `json:"federation_resolvers"`
+	InvalidationJobs                                  []tc.InvalidationJobInput               `json:"invalidationJobs"`
 	Origins                                           []tc.Origin                             `json:"origins"`
 	Profiles                                          []tc.Profile                            `json:"profiles"`
 	Parameters                                        []tc.Parameter                          `json:"parameters"`
@@ -74,5 +75,4 @@ type TrafficControl struct {
 	SteeringTargets                                   []tc.SteeringTargetNullable             `json:"steeringTargets"`
 	Serverchecks                                      []tc.ServercheckRequestNullable         `json:"serverchecks"`
 	Users                                             []tc.User                               `json:"users"`
-	InvalidationJobs                                  []tc.InvalidationJobInput               `json:"invalidationJobs"`
 }
diff --git a/cache-config/testing/ort-tests/tcdata/topologies.go b/cache-config/testing/ort-tests/tcdata/topologies.go
index 23e28c123..55331bd8f 100644
--- a/cache-config/testing/ort-tests/tcdata/topologies.go
+++ b/cache-config/testing/ort-tests/tcdata/topologies.go
@@ -45,7 +45,6 @@ func (r *TCData) CreateTestTopologies(t *testing.T) {
 		if !reflect.DeepEqual(topology, postResponse.Response) {
 			t.Fatalf("Topology in response should be the same as the one POSTed. expected: %v, actual: %v", topology, postResponse.Response)
 		}
-		t.Logf("Response: %+v", *postResponse)
 	}
 }
 
diff --git a/cache-config/testing/ort-tests/tcdata/withobjs.go b/cache-config/testing/ort-tests/tcdata/withobjs.go
index 0e6ea1b99..b3bf3337d 100644
--- a/cache-config/testing/ort-tests/tcdata/withobjs.go
+++ b/cache-config/testing/ort-tests/tcdata/withobjs.go
@@ -36,6 +36,7 @@ const (
 	Divisions
 	FederationResolvers
 	FederationUsers
+	InvalidationJobs
 	Origins
 	Parameters
 	PhysLocations
@@ -85,6 +86,7 @@ func (r *TCData) WithObjs(t *testing.T, objs []TCObj, f func()) {
 		Divisions:                            {r.CreateTestDivisions, r.DeleteTestDivisions},
 		FederationUsers:                      {r.CreateTestFederationUsers, r.DeleteTestFederationUsers},
 		FederationResolvers:                  {r.CreateTestFederationResolvers, r.DeleteTestFederationResolvers},
+		InvalidationJobs:                     {r.CreateTestInvalidationJobs, r.DeleteTestInvalidationJobs},
 		Origins:                              {r.CreateTestOrigins, r.DeleteTestOrigins},
 		Parameters:                           {r.CreateTestParameters, r.DeleteTestParameters},
 		PhysLocations:                        {r.CreateTestPhysLocations, r.DeleteTestPhysLocations},
diff --git a/cache-config/testing/ort-tests/to_requester_test.go b/cache-config/testing/ort-tests/to_requester_test.go
index c4edb596c..cbc1e521f 100644
--- a/cache-config/testing/ort-tests/to_requester_test.go
+++ b/cache-config/testing/ort-tests/to_requester_test.go
@@ -23,6 +23,7 @@ import (
 	"testing"
 
 	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
@@ -111,7 +112,7 @@ func TestTORequester(t *testing.T) {
 		if err != nil {
 			t.Fatalf("t3c-request exec failed: %v", err)
 		}
-		var serverStatus tc.ServerUpdateStatus
+		var serverStatus atscfg.ServerUpdateStatus
 		err = json.Unmarshal([]byte(output), &serverStatus)
 		if err != nil {
 			t.Fatalf("failed to parse t3c-request output: %v", err)
diff --git a/cache-config/testing/ort-tests/to_updater_test.go b/cache-config/testing/ort-tests/to_updater_test.go
index b56bd6b08..5384818e8 100644
--- a/cache-config/testing/ort-tests/to_updater_test.go
+++ b/cache-config/testing/ort-tests/to_updater_test.go
@@ -21,9 +21,11 @@ import (
 	"os/exec"
 	"strconv"
 	"testing"
+	"time"
 
 	"github.com/apache/trafficcontrol/cache-config/testing/ort-tests/tcdata"
-	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestTOUpdater(t *testing.T) {
@@ -32,14 +34,14 @@ func TestTOUpdater(t *testing.T) {
 		tcdata.Profiles, tcdata.ProfileParameters, tcdata.Statuses,
 		tcdata.Divisions, tcdata.Regions, tcdata.PhysLocations,
 		tcdata.CacheGroups, tcdata.Servers, tcdata.Topologies,
-		tcdata.DeliveryServices}, func() {
+		tcdata.DeliveryServices, tcdata.InvalidationJobs}, func() {
 
 		// retrieve the current server status
 		output, err := runRequest(DefaultCacheHostName, CMDUpdateStatus)
 		if err != nil {
 			t.Fatalf("t3c-request failed: %v", err)
 		}
-		var serverStatus tc.ServerUpdateStatus
+		var serverStatus atscfg.ServerUpdateStatus
 		err = json.Unmarshal([]byte(output), &serverStatus)
 		if err != nil {
 			t.Fatalf("failed to parse t3c-request output: %v", err)
@@ -47,17 +49,17 @@ func TestTOUpdater(t *testing.T) {
 		if serverStatus.HostName != DefaultCacheHostName {
 			t.Fatalf("expected server status host name to be '%s', actual: %s", DefaultCacheHostName, serverStatus.HostName)
 		}
-		if serverStatus.RevalPending != false {
-			t.Fatal("expected RevalPending to be 'false'")
+		if serverStatus.RevalPending != true { // should be true since invalidation jobs were queued
+			t.Fatal("expected RevalPending to be 'true'")
 		}
 		if serverStatus.UpdatePending != false {
 			t.Fatal("expected UpdatePending to be 'false'")
 		}
 
 		// change the server update status
-		err = ExecTOUpdater(DefaultCacheHostName, false, true)
+		err = tcd.QueueUpdatesForServer(DefaultCacheHostName, true)
 		if err != nil {
-			t.Fatalf("t3c-update failed: %v", err)
+			t.Fatalf("failed to set config update: %v", err)
 		}
 		// verify the update status is now 'true'
 		output, err = runRequest(DefaultCacheHostName, CMDUpdateStatus)
@@ -68,15 +70,42 @@ func TestTOUpdater(t *testing.T) {
 		if err != nil {
 			t.Fatalf("failed to parse t3c-request output: %v", err)
 		}
-		if serverStatus.RevalPending != false {
-			t.Fatal("expected RevalPending to be 'false'")
+		if serverStatus.RevalPending != true { // should be true since invalidation jobs were queued
+			t.Fatal("expected RevalPending to be 'true'")
 		}
 		if serverStatus.UpdatePending != true {
 			t.Fatal("expected UpdatePending to be 'true'")
 		}
 
+		// set config apply time to the config update time to signal the update was applied
+		err = ExecTOUpdater(DefaultCacheHostName, serverStatus.ConfigUpdateTime, nil, util.BoolPtr(false), nil)
+		if err != nil {
+			t.Fatalf("t3c-update failed: %v", err)
+		}
+		// verify the update status is now 'false'
+		output, err = runRequest(DefaultCacheHostName, CMDUpdateStatus)
+		if err != nil {
+			t.Fatalf("t3c-request failed: %v", err)
+		}
+		err = json.Unmarshal([]byte(output), &serverStatus)
+		if err != nil {
+			t.Fatalf("failed to parse t3c-request output: %v", err)
+		}
+		if serverStatus.RevalPending != true { // should be true since invalidation jobs were queued
+			t.Fatal("expected RevalPending to be 'true'")
+		}
+		if serverStatus.UpdatePending != false {
+			t.Fatal("expected UpdatePending to be 'false'")
+		}
+		if serverStatus.ConfigApplyTime != nil && serverStatus.ConfigUpdateTime != nil {
+			if !(*serverStatus.ConfigApplyTime).Equal(*serverStatus.ConfigUpdateTime) {
+				t.Fatalf("failed to set config apply time.\nExpc: %v\nRecv: %v", *serverStatus.ConfigUpdateTime, *serverStatus.ConfigApplyTime)
+			}
+		}
+
 		// now change the reval stat and put server update status back
-		err = ExecTOUpdater(DefaultCacheHostName, true, false)
+		// set config apply time to the config update time to signal the update was applied
+		err = ExecTOUpdater(DefaultCacheHostName, nil, serverStatus.RevalidateUpdateTime, nil, util.BoolPtr(false))
 		if err != nil {
 			t.Fatalf("t3c-update failed: %v", err)
 		}
@@ -89,17 +118,22 @@ func TestTOUpdater(t *testing.T) {
 		if err != nil {
 			t.Fatalf("failed to parse t3c-request output: %v", err)
 		}
-		if serverStatus.RevalPending != true {
+		if serverStatus.RevalPending != false {
 			t.Fatal("expected RevalPending to be 'false'")
 		}
 		if serverStatus.UpdatePending != false {
-			t.Fatal("expected UpdatePending to be 'true'")
+			t.Fatal("expected UpdatePending to be 'false'")
+		}
+		if serverStatus.RevalidateApplyTime != nil && serverStatus.RevalidateUpdateTime != nil {
+			if !(*serverStatus.RevalidateApplyTime).Equal(*serverStatus.RevalidateUpdateTime) {
+				t.Fatalf("failed to set reval apply time.\nExpc: %v\nRecv: %v", *serverStatus.RevalidateUpdateTime, *serverStatus.RevalidateApplyTime)
+			}
 		}
 
 	})
 }
 
-func ExecTOUpdater(host string, reval_status bool, update_status bool) error {
+func ExecTOUpdater(host string, configApplyTime, revalApplyTime *time.Time, configApplyBool, revalApplyBool *bool) error {
 	args := []string{
 		"update",
 		"--traffic-ops-insecure=true",
@@ -109,9 +143,23 @@ func ExecTOUpdater(host string, reval_status bool, update_status bool) error {
 		"--traffic-ops-url=" + tcd.Config.TrafficOps.URL,
 		"--cache-host-name=" + host,
 		"-vv",
-		"--set-reval-status=" + strconv.FormatBool(reval_status),
-		"--set-update-status=" + strconv.FormatBool(update_status),
 	}
+	if configApplyTime != nil {
+		args = append(args, "--set-config-apply-time="+(*configApplyTime).Format(time.RFC3339Nano))
+	}
+	if revalApplyTime != nil {
+		args = append(args, "--set-reval-apply-time="+(*revalApplyTime).Format(time.RFC3339Nano))
+	}
+
+	// *** Compatability requirement until ATC (v7.0+) is deployed with the timestamp features
+	if configApplyBool != nil {
+		args = append(args, "--set-update-status="+strconv.FormatBool(*configApplyBool))
+	}
+	if revalApplyBool != nil {
+		args = append(args, "--set-reval-status="+strconv.FormatBool(*revalApplyBool))
+	}
+	// ***
+
 	cmd := exec.Command("t3c", args...)
 	var out bytes.Buffer
 	var errOut bytes.Buffer
diff --git a/cache-config/testing/ort-tests/traffic_ops_ort_test.go b/cache-config/testing/ort-tests/traffic_ops_ort_test.go
index 4bd646486..8fa43276a 100644
--- a/cache-config/testing/ort-tests/traffic_ops_ort_test.go
+++ b/cache-config/testing/ort-tests/traffic_ops_ort_test.go
@@ -33,12 +33,9 @@ import (
 
 var (
 	Config             config.Config
-	testData           tcdata.TrafficControl
 	includeSystemTests bool
+	tcd                *tcdata.TCData
 )
-var tcd *tcdata.TCData
-
-var TCD *tcdata.TCData
 
 const cfgFmt = `Using Config values:
 	TO Config File:              %s
@@ -53,7 +50,7 @@ const cfgFmt = `Using Config values:
 
 func TestMain(m *testing.M) {
 	tcd = tcdata.NewTCData()
-	configFileName := flag.String("cfg", "traffic-ops-test.conf", "The config file path")
+	configFileName := flag.String("cfg", "conf/traffic-ops-test.conf", "The config file path")
 	tcFixturesFileName := flag.String("fixtures", "tc-fixtures.json", "The test fixtures for the API test tool")
 	cliIncludeSystemTests := *flag.Bool("includeSystemTests", false, "Whether to enable tests that have environment dependencies beyond a database")
 	flag.Parse()
diff --git a/traffic_ops/testing/api/v4/servers_test.go b/traffic_ops/testing/api/v4/servers_test.go
index 01e58dc5b..597044422 100644
--- a/traffic_ops/testing/api/v4/servers_test.go
+++ b/traffic_ops/testing/api/v4/servers_test.go
@@ -1267,9 +1267,6 @@ func UpdateTestServers(t *testing.T) {
 	if *respServer.TypeID != *remoteServer.TypeID {
 		t.Errorf("Type ID is not updated while updating the servers")
 	}
-	if *respServer.UpdPending != *remoteServer.UpdPending {
-		t.Errorf("Updpending is not updated while updating the servers")
-	}
 
 	//Check to verify XMPPID never gets updated
 	remoteServer.XMPPID = &updatedXMPPID
diff --git a/traffic_ops/testing/api/v4/serverupdatestatus_test.go b/traffic_ops/testing/api/v4/serverupdatestatus_test.go
index eac7ebb4c..8d7b42689 100644
--- a/traffic_ops/testing/api/v4/serverupdatestatus_test.go
+++ b/traffic_ops/testing/api/v4/serverupdatestatus_test.go
@@ -22,6 +22,7 @@ import (
 	"net/url"
 	"strconv"
 	"testing"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
@@ -376,7 +377,7 @@ func TestSetServerUpdateStatuses(t *testing.T) {
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Add("hostName", *testServer.HostName)
-		testVals := func(queue *bool, reval *bool) {
+		testVals := func(configApply, revalApply *time.Time) {
 			resp, _, err := TOSession.GetServers(opts)
 			if err != nil {
 				t.Errorf("cannot get Server by name '%s': %v - alerts: %+v", *testServer.HostName, err, resp.Alerts)
@@ -384,17 +385,31 @@ func TestSetServerUpdateStatuses(t *testing.T) {
 				t.Fatalf("GET Server expected 1, actual %v", len(resp.Response))
 			}
 
-			existingServer := resp.Response
+			beforeServer := resp.Response[0]
 
-			if existingServer[0].UpdPending == nil {
+			// Ensure baseline
+			if beforeServer.UpdPending == nil {
 				t.Fatalf("Server '%s' had nil UpdPending before update status change", *testServer.HostName)
 			}
-			if existingServer[0].RevalPending == nil {
+			if beforeServer.RevalPending == nil {
 				t.Fatalf("Server '%s' had nil RevalPending before update status change", *testServer.HostName)
 			}
+			if beforeServer.ConfigUpdateTime == nil {
+				t.Fatalf("Server '%s' had nil ConfigUpdateTime before update status change", *testServer.HostName)
+			}
+			if beforeServer.ConfigApplyTime == nil {
+				t.Fatalf("Server '%s' had nil ConfigApplyTime before update status change", *testServer.HostName)
+			}
+			if beforeServer.RevalUpdateTime == nil {
+				t.Fatalf("Server '%s' had nil RevalUpdateTime before update status change", *testServer.HostName)
+			}
+			if beforeServer.RevalApplyTime == nil {
+				t.Fatalf("Server '%s' had nil RevalApplyTime before update status change", *testServer.HostName)
+			}
 
-			if alerts, _, err := TOSession.SetUpdateServerStatuses(*testServer.HostName, queue, reval, client.RequestOptions{}); err != nil {
-				t.Fatalf("UpdateServerStatuses error expected: nil, actual: %v - alerts: %+v", err, alerts.Alerts)
+			// Make change
+			if alerts, _, err := TOSession.SetUpdateServerStatusTimes(*testServer.HostName, configApply, revalApply, client.RequestOptions{}); err != nil {
+				t.Fatalf("SetUpdateServerStatusTimes error. expected: nil, actual: %v - alerts: %+v", err, alerts.Alerts)
 			}
 
 			resp, _, err = TOSession.GetServers(opts)
@@ -404,44 +419,41 @@ func TestSetServerUpdateStatuses(t *testing.T) {
 				t.Fatalf("GET Server expected 1, actual %v", len(resp.Response))
 			}
 
-			newServer := resp.Response
+			afterServer := resp.Response[0]
 
-			if newServer[0].UpdPending == nil {
+			if afterServer.UpdPending == nil {
 				t.Fatalf("Server '%s' had nil UpdPending after update status change", *testServer.HostName)
 			}
-			if newServer[0].RevalPending == nil {
+			if afterServer.RevalPending == nil {
 				t.Fatalf("Server '%s' had nil RevalPending after update status change", *testServer.HostName)
 			}
 
-			if queue != nil {
-				if *newServer[0].UpdPending != *queue {
-					t.Errorf("set queue update pending to %v, but then got server %v", *queue, *newServer[0].UpdPending)
-				}
-			} else {
-				if *newServer[0].UpdPending != *existingServer[0].UpdPending {
-					t.Errorf("set queue update pending with nil (don't update), but then got server %v which didn't match pre-update value %v", *newServer[0].UpdPending, *existingServer[0].UpdPending)
+			// Ensure values were actually set
+			if configApply != nil {
+				if afterServer.ConfigApplyTime == nil || !afterServer.ConfigApplyTime.Equal(*configApply) {
+					t.Errorf("Failed to set server's ConfigApplyTime. expected: %v actual: %v", *configApply, afterServer.ConfigApplyTime)
 				}
 			}
-			if reval != nil {
-				if *newServer[0].RevalPending != *reval {
-					t.Errorf("set reval update pending to %v, but then got server %v", *reval, *newServer[0].RevalPending)
-				}
-			} else {
-				if *newServer[0].RevalPending != *existingServer[0].RevalPending {
-					t.Errorf("set reval update pending with nil (don't update), but then got server %v which didn't match pre-update value %v", *newServer[0].RevalPending, *existingServer[0].RevalPending)
+			if revalApply != nil {
+				if afterServer.RevalApplyTime == nil || !afterServer.RevalApplyTime.Equal(*revalApply) {
+					t.Errorf("Failed to set server's RevalApplyTime. expected: %v actual: %v", *revalApply, afterServer.RevalApplyTime)
 				}
 			}
+
 		}
 
-		testVals(util.BoolPtr(true), util.BoolPtr(true))
-		testVals(util.BoolPtr(true), util.BoolPtr(false))
-		testVals(util.BoolPtr(false), util.BoolPtr(false))
-		testVals(nil, util.BoolPtr(true))
-		testVals(nil, util.BoolPtr(false))
-		testVals(util.BoolPtr(true), nil)
-		testVals(util.BoolPtr(false), nil)
+		// Postgres stores microsecond precision. There is also some discussion around MacOS losing
+		// precision as well. The nanosecond precision is accurate within go one linux however,
+		// but round trips to and from the database may result in an inaccurate Equals comparison
+		// with the loss of precision. Also, it appears to Round and not Truncate.
+		now := time.Now().Round(time.Microsecond)
+
+		// Test setting the values works as expected
+		testVals(util.TimePtr(now), nil) // configApply
+		testVals(nil, util.TimePtr(now)) // revalApply
 
-		if _, _, err := TOSession.SetUpdateServerStatuses(*testServer.HostName, nil, nil, client.RequestOptions{}); err == nil {
+		// Test sending all nils. Should fail
+		if _, _, err := TOSession.SetUpdateServerStatusTimes(*testServer.HostName, nil, nil, client.RequestOptions{}); err == nil {
 			t.Errorf("UpdateServerStatuses with (nil,nil) expected error, actual nil")
 		}
 	})
@@ -457,7 +469,7 @@ func TestSetTopologiesServerUpdateStatuses(t *testing.T) {
 		)
 		cacheGroupNames := []string{edgeCacheGroup, otherEdgeCacheGroup, midCacheGroup}
 		cachesByCacheGroup := map[string]tc.ServerV40{}
-		updateStatusByCacheGroup := map[string]tc.ServerUpdateStatus{}
+		updateStatusByCacheGroup := map[string]tc.ServerUpdateStatusV40{}
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("name", topologyName)
diff --git a/traffic_ops/testing/api/v4/tc-fixtures.json b/traffic_ops/testing/api/v4/tc-fixtures.json
index 2f486eeea..d686a8c4d 100644
--- a/traffic_ops/testing/api/v4/tc-fixtures.json
+++ b/traffic_ops/testing/api/v4/tc-fixtures.json
@@ -4907,6 +4907,410 @@
             "updPending": false,
             "xmppId": "atlanta-mid-02\\\\@ocdn.kabletown.net",
             "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "revalpnd-legacy-true",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.43.1/30",
+                            "gateway": "127.0.43.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:43:8::1/64",
+                            "gateway": "2345:1234:43:8::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": true,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false,
+            "xmppId": "revalpnd-legacy-true\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "updatepnd-legacy-true",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.44.1/30",
+                            "gateway": "127.0.44.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:44:8::1/64",
+                            "gateway": "2345:1234:44:8::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": true,
+            "xmppId": "revalpnd-legaacy-true\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+			"configUpdateTime": "2022-01-01T17:00:00-07:00",
+			"configApplyTime": "1969-12-31T17:00:00-07:00",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "config-update-time",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.45.1/30",
+                            "gateway": "127.0.45.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:45:8::1/64",
+                            "gateway": "2345:1234:45:8::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false,
+            "xmppId": "config-update-time\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+			"configUpdateTime": "2022-01-01T17:00:00-07:00",
+			"configApplyTime": "1969-12-31T17:00:00-07:00",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "config-update-time-no-updpend",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.45.2/30",
+                            "gateway": "127.0.45.2",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:45:8::2/64",
+                            "gateway": "2345:1234:45:8::2",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "xmppId": "config-update-time-no-updpend\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "reval-update-time",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.46.1/30",
+                            "gateway": "127.0.46.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:46:8::1/64",
+                            "gateway": "2345:1234:46:8::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+			"revalUpdateTime": "2022-01-01T17:00:00-07:00",
+			"revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false,
+            "xmppId": "config-update-time\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "reval-update-time-no-revalpend",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.46.2/30",
+                            "gateway": "127.0.46.2",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:46:8::2/64",
+                            "gateway": "2345:1234:46:8::2",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+			"revalUpdateTime": "2022-01-01T17:00:00-07:00",
+			"revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false,
+            "xmppId": "config-update-time\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+			"configUpdateTime": "2022-01-01T17:00:00-07:00",
+			"configApplyTime": "1969-12-31T17:00:00-07:00",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "config-reval-update-time",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.47.1/30",
+                            "gateway": "127.0.47.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:47:8::1/64",
+                            "gateway": "2345:1234:47:8::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+			"revalUpdateTime": "2022-01-01T17:00:00-07:00",
+			"revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false,
+            "xmppId": "config-update-time\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
+        },
+        {
+            "cachegroup": "cachegroup1",
+            "cdnName": "cdn1",
+			"configUpdateTime": "2022-01-01T17:00:00-07:00",
+			"configApplyTime": "1969-12-31T17:00:00-07:00",
+            "domainName": "ga.atlanta.kabletown.net",
+            "guid": null,
+            "hostName": "config-reval-update-time-only",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.47.2/30",
+                            "gateway": "127.0.47.2",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:47:8::2/64",
+                            "gateway": "2345:1234:47:8::2",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0",
+                    "routerHostName": "router1",
+                    "routerPort": "9000"
+                }
+            ],
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+			"revalUpdateTime": "2022-01-01T17:00:00-07:00",
+			"revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "xmppId": "config-update-time\\\\@ocdn.kabletown.net",
+            "xmppPasswd": "X"
         }
     ],
     "serverCapabilities": [
diff --git a/traffic_ops/traffic_ops_golang/server/servers_test.go b/traffic_ops/traffic_ops_golang/server/servers_test.go
index 5a051c045..4cbf215f3 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_test.go
@@ -379,6 +379,7 @@ func TestGetMidServers(t *testing.T) {
 	}
 
 	cols2 := test.ColsFromStructByTag("db", tc.CommonServerProperties{})
+	cols2 = append(cols2, "config_update_time", "config_apply_time", "revalidate_update_time", "revalidate_apply_time")
 	rows2 := sqlmock.NewRows(cols2)
 
 	cgs := []tc.CacheGroup{}
@@ -477,6 +478,10 @@ func TestGetMidServers(t *testing.T) {
 		ts.UpdPending,
 		ts.XMPPID,
 		ts.XMPPPasswd,
+		ts.ConfigUpdateTime,
+		ts.ConfigApplyTime,
+		ts.RevalUpdateTime,
+		ts.RevalApplyTime,
 	)
 
 	mock.ExpectBegin()
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
index bfbe4ff11..6f20fc6c2 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
@@ -23,6 +23,7 @@ import (
 	"context"
 	"reflect"
 	"testing"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
@@ -41,8 +42,12 @@ func TestGetServerUpdateStatus(t *testing.T) {
 	defer db.Close()
 
 	mock.ExpectBegin()
-	serverStatusRow := sqlmock.NewRows([]string{"id", "host_name", "type", "server_reval_pending", "use_reval_pending", "upd_pending", "status", "parent_upd_pending", "parent_reval_pending"})
-	serverStatusRow.AddRow(1, "host_name_1", "EDGE", true, true, true, "ONLINE", true, false)
+	serverStatusRow := sqlmock.NewRows([]string{"id", "host_name", "type", "server_reval_pending", "use_reval_pending",
+		"server_upd_pending", "status", "parent_upd_pending", "parent_reval_pending",
+		"config_update_time", "config_apply_time", "revalidate_update_time", "revalidate_apply_time"})
+	serverStatusRow.AddRow(1, "host_name_1", "EDGE", true, true,
+		true, "ONLINE", true, false,
+		time.Now(), time.Now(), time.Now(), time.Now())
 
 	mock.ExpectQuery("SELECT").WillReturnRows(serverStatusRow)
 	mock.ExpectCommit()