Jed Wallace scored one goal and made another as Millwall halted Bristol City's Sky Bet Championship play-off push with a 2-1 win at Ashton Gate.
The visitors took an 11th minute lead when Mahlon Romeo's pass allowed Wallace to advance into the penalty area and, although his right-footed shot lacked pace, it beat Dan Bentley's dive and outstretched left hand.
Having looked comfortable at the back, Millwall went further in front on 70 minutes when centre-back Jake Cooper rose highest to head home a Wallace free-kick.
But substitute Callum O'Dowda gave City hope with a right-footed shot from 15 yards six minutes from time, which went in off a post after goalkeeper Bartosz Bialkowski got a hand to it.
And, in a frantic finish, Bialkowski produced a fine save with an outstretched leg to deny another substitute, Pedro Pereira, an injury-time equaliser.
Millwall, who are now six matches unbeaten, were good value for their interval lead, having created the majority of the chances against a home side looking strangely lethargic and out of touch.
City's best opportunity came in the opening minutes when a defensive error allowed Andreas Weimann a free run on goal.
The striker elected to try and round Bialkowski and then hesitated having been driven wide, allowing defenders to eventually crowd him out.
Jon Dadi Bodvarsson was off-target with a header and low drive as Millwall pressed to build on the Wallace goal. Tom Bradshaw also went close with a 22nd-minute shot.
City's attacks were ponderous as Gary Rowett's plan of playing three centre-backs and crowding the midfield worked to a tee.
Josh Brownhill briefly roused home fans with a 30th minute shot from distance, which gathered pace off the wet surface and flashed just the wrong side of a post.
But it was a rare moment of first-half concern for Bialkowski and Romeo might have increased Millwall's lead on 41 minutes when receiving the ball in yards of space on the right and striding forward to fire high and wide.
Lee Johnson presumably fired up his troops at the interval and a slick 48th-minute move ended with Adam Nagy shooting wide from the edge of the box.
But Millwall continued to look solid and City's head coach made his first change of 65 minutes, sending on Kasey Palmer for Han-Noah Massengo in midfield.
It was Palmer who conceded the free-kick that led to Cooper's goal. And it was only after O'Dowda's strike that the hosts really came to life, piling on pressure, which proved too little too late.
Before kick-off, both sets of fans joined in a minute's applause in support of injured City loan signing Benik Afobe, whose two-year-old daughter Amora passed away recently.
What the managers said...
Bristol City's Lee Johnson: "It was too little too late from us. I will be asking three or four players why they were so below par at the start. Millwall are a good side with a good manager and made things difficult for us. But for most of the game we were not ourselves.
"The spirit in the camp showed through with the late onslaught, but it didn't prove enough and we can have no complaints. It's a setback. But we have no divine right to beat teams like Millwall and you have to expect strange results from time to time in a league as competitive as the Championship.
Millwall's Gary Rowett: "It seems Jed Wallace is instrumental in everything we achieve at the moment. His recent stats are unbelievable. But he would be the first to admit that 10 other players are providing the structure from which he can score and assist.
"Our first-half performance tonight was outstanding against a very good side. I thought we were solid and always carried a threat going forward. The only thing that disappoints me is that when we concede goals we tend to drop off, which left us hanging on in this game.