I don't have any way to test my assumptions right now, but I assume you're seeing the side effects of the way ParseDateTime() handles a Java object, and the differences of that function between CF2016 and CF2021/18.
Since expriry_date is an actual datetime object in your database, cfquery from the database will give you a sql object instead of just a string that looks like a date. Since it has both date and time components, I believe that JDBC will send a java.sql.Timestamp object back to CF.
The purpose of ParseDateTime() is for converting a string to a date object, and even though it can "format" a date object, that's not what it's intended for. It's initial assumption is that it's being given a string, and because CF is dynamically typed, it will usually try to implicitly convert a value into the correct type. And since the database value is date-ish, CF will try to make it appropriately look like a string. At least that's the way it was intended to work. And it looks like that is what is happening in <CF2016 and Lucee, and the value is getting a "{ts...".
It appears that CF2021/18 is receiving a date object and not really doing much with it. It seems to be just passing the Java object (without the "{ts...") back out, and when ColdFusion tries to convert it to a string, it's not a ColdFusion timestamp variable so there is no "{ts...".
DEMO CODE:
<cfscript>
/*** CREATE QUERY OBJECT ***/
theDateQuery = QueryNew("dt","timestamp",[{"dt":createODBCDateTime('2021-04-09 12:01:02.345')}]) ;
writeDump(theDateQuery);
/*** SET QUERY RESULT TO A VARIABLE ***/
dobj = theDateQuery.dt[1] ;
writeOutput("dobj: " & dobj & " ("& getMetadata(dobj).getName() & ")<br>");
/*** CONVERT QUERY VARIABLE TO A STRING ***/
dstr = theDateQuery.dt[1].toString() ;
writeOutput("dstr: " & dstr & " ("& getMetadata(dstr).getName() & ")<br>");
/*** PARSEDATETIME OF OBJECT ***/
x = parseDateTime(dobj) ;
writeOutput("x: " & x & " ("& getMetadata(x).getName() & ")<br>");
/*** PARSEDATETIME OF STRING ***/
y = parseDateTime(dstr) ;
writeOutput("y: " & y & " ("& getMetadata(y).getName() & ")<br>");
</cfscript>
CF 2016 and lower
https://trycf.com/gist/c9c35dccb04f91cd6c06e4082ed306ca/acf2016?theme=monokai
Your database is giving the ColdFusion query back a Java object, which is going into the ParseDateTime function, and CF is doing its magic then giving you back a ColdFusion date object, which looks like a CF timestamp that is easily coerced into what you want it to display.
dobj: 2021-04-09 12:01:02.345 (java.sql.Timestamp)
x: {ts '2021-04-09 12:01:02'} (coldfusion.runtime.OleDateTime)
CF2021 (and CF2018)
https://trycf.com/gist/4c9bbc036a63a135962f0912b8591e00/acf2021?theme=monokai (trycf link for CF2021 seems to be defaulting to Lucee5, so you'll have to reselect CF2021)
It looks like the behavior changed slightly in CF2018. I don't know the inner workings of the ParseDateTime functions, but I'm assuming that in earlier versions, it would convert the date object to a date string before it converted it into a ColdFusion object on return. As of 2018, it appears that if it receives a Java data object, it recognizes that it's already a date object and just returns it (bypassing a conversion to a ColdFusion date string and then object). This is seems to be more in line with the originally stated behavior. And since the Java timestamp value doesn't have "{ts...", but CF does, it changes the way a string us ultimately created from the function's return value. This is possibly a compatibility bug (since the behavior is subtly different), but I'm not completely sure.
Regardless, it seems to be caused by the simple fact that ParseDateTime is being used here for something other than its intended purpose. Just like using TimeFormat(now(),"yyyy-MMM-dd hh:nn:ss"). ACF will return a string with the Year and Day masked, even though that's not something that TimeFormat() is intended for (NOTE: Lucee seems to handle it correctly). If a function is used in an unexpected manner, it shouldn't be expected to return consistent results in different versions.
And that's where I get kinda iffy on whether it should be a bug or not. In the past, most ColdFusion functions didn't much care if a "date" was a string or an object. It seems that CF2018+ does. Or at least treats them differently.
Ultimately, your code should probably be fixed to use functions the way they were meant to be used. However, I understand that might be a bit much. You might be able to fix your current issue by explicitly converting the query object into a string before you parse it.
ParseDateTime(qryDates.expiry_date.toString())
One other thing to note is that in CF2016, if your datetime value had a millisecond component, that would be stripped off in your final ParseDateTime value. However, in CF2021/18, since the Java value doesn't appear to be changed, the milliseconds component will still be in your return value. toString() should also fix that, and you'll get exactly what you had before.