You can use tr to transform the stream into one you can grep normally:
stream | tr 'x\n' '\0x' | grep -qz xxx
This turns all x bytes into null bytes, and all linefeed bytes into xs, which can be grepped out as usual. That is, it moves one step along the path linefeed -> x -> null, so a sequence of three linefeeds will now be a sequence of three xs, and no other x bytes will occur (they will have become nulls terminating lines for the grep).
This works with POSIX tr, but grep -z is an extension. You may not need it - the separation behaviour isn't required here - and most greps will handle binary data, but POSIX grep is only required to work on text files so you're going to be depending on an extension one way or another.
If your real data is a text file, or just doesn't depend on binary-safe behaviour, you can probably survive on just
stream | tr 'x\n' '\nx' | grep -q xxx
- that is, just swapping the two bytes. This is nearly POSIX-compatible, but will likely work in practice just about anywhere (the issue is that the final line won't be terminated correctly, so it's not a text file, so grep isn't strictly required to accept it).
One possible issue in either case is that a file with no existing x bytes will be considered as one very long line, which may exceed the limits your grep implementation will handle. Choosing another expected-to-be-common byte may work around that.
I was surprised that your original grep -qz $'\n\n\n' command didn't work, but it had a false-positive problem for me - it seemed to behave like grep -qz '' and always matched. I'm not sure why that is.
grep -Pzq '\n\n\n'seems to work... not sure the whys and wherefores – steeldriver Jan 08 '19 at 03:34