The easiest method that performs well and gives a lot of flexibility is to use a SQL database, e.g. an in-memory SQLite database.
Using such, you can trivially find all pending matches that meet some criteria, insert a pending match, and remove "stale" matches that are too old (indicating no match was found).
Define a table that has a column for your match criteria and metadata, like so:
| id | username | created | gamemode | gamesize | gamelength |
| 77 | bob52 | 08:17:23 | ctf | 16 | 15 |
| 63 | johnny77 | 08:16:59 | dm | 32 | 15 |
| 59 | theboy1 | 08:16:49 | ctf | 32 | 30 |
When you get a request for a new match, do a query for rows matching that pattern, limiting yourself to the oldest match (depending on the sophistication of your criteria and how you select "good" matches, you may end up with a queue building up, e.g. where two players A and B don't match each other but C matches both, and you want FIFO behavior in these cases). Form a match on that pair. If there are no matches, insert the request into the table to be paired with the next matching request.
You can periodically clean out old entries that might accumulate. In general you don't need a timeout, though you do need a way to cancel match-making (and remove any rows for that client). There's no reason to always time out at 30 seconds an dmake a player resubmit their request over and over, especially as it might just be that matches could be found by just waiting a bit. If you are having long match-making times, that means your algorithm is bad, or your game is unpopular. Or you have unpopular game modes; if very few people play 32-person ctf with a 30min timer, just remove that as a valid option. The more game modes you have the more your playerbase is spread apart and the harder it is to make a match.
One nice advantage of SQL is that you can easily make some criteria less explicit. If a player has no preference on match length, that column simply isn't queried when looking for matches, and a value of NULL can be created for that column if the request is queued. Matches can have a "gamelength IS NULL or gamelength=:length" to the query so folks who don't care about game length can be matched with those who have a preference, which is a fine match.
A query from a user looking for any ctf match thus might look like:
SELECT id FROM matches WHERE gamemode IS NULL or gamemode='ctf' ORDER BY created ASC LIMIT 1;
(note that LIMIT is a non-standard extension, but a lot of DBs support it.)
Replace the id as appropriate. This might be the IP of the host user, a unique id for a dedicated server instance, or a per-match id used to coordinate users while spinning up a dedicated server instance. Whatever.
You can also keep matches in the queue for running games and have columns to track how many players are in the match (on each team) already and how old the match is, making is easy to selectively place someone into an in-progress game if a desired team color/type/whatever is in need of more people.
Using a SQL database also makes it easy to just move from in-memory SQLite to a full RDBMS if ever your game needs such. While I recommend strongly to avoid using a single SQL database for an entire world-wide game, you could easily having a single DB environment for each region of a game. RDBMS are not the most efficient ways to implement queues but they work well enough for most games, especially if you have a lot of match criteria.
Another option that can work a bit better in some cases is to create a list of queues. Create one queue for each game configuration choice. Eg. a ctf 32-player queue, ctf 16-player queue, ctf don't-care-how-many-people queue, a dm 32-player queue, etc. When a request for a match comes in, try each appropriate queue. Take the oldest match found (again, maintain FIFO ordering).
This works well if there are a smaller finite number of game mode configurations, and it can be scaled in some interesting ways, but in general is more work to do (especially to do well) than just using a SQL database.