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.