You already know how to run tcpdump. You already know Wireshark. tshark is the awkward middle child that most engineers skip, and skipping it costs you real time, because it is the only one of the three that will dissect HTTP, decode a TLS handshake, follow a TCP stream and hand you a throughput histogram without ever leaving the SSH session you are already in.
This article is part of the Linux networking commands guide. Everything below was captured on a Debian 13 host bridged into a CML topology: the host sits at 10.77.0.100, an nginx server (WEB1) sits at 10.77.2.10 two router hops away, and an iperf3 server (SRV1) sits at 10.77.3.10 three hops away. Every byte of output is from that lab.
tshark against tcpdump against Wireshark
They are not competitors so much as three points on a curve, and the honest answer to "which one" is usually "whichever is already installed."
The workflow that actually happens in practice is a blend: capture with tcpdump -w or tshark -w on the remote host, triage with tshark in place to decide whether the file is worth moving, then pull the interesting file down for Wireshark. If you have not read it yet, tcpdump for network engineers covers the capture side in depth, and the capture filter syntax there is exactly the syntax tshark uses for -f.
Picking an interface with -D
tshark needs to be told what to listen to, and -D tells you what is available:
j@llmbits:~$ sudo tshark -D
1. ens192
2. ens224
3. any
4. lo (Loopback)
5. bluetooth-monitor
6. nflog
7. nfqueue
8. dbus-system
9. dbus-session
10. ciscodump (Cisco remote capture)
11. dpauxmon (DisplayPort AUX channel monitor capture)
12. randpkt (Random packet generator)
13. sdjournal (systemd Journal Export)
14. sshdump (SSH remote capture)
15. udpdump (UDP Listener remote capture)
16. wifidump (Wi-Fi remote capture)Note what is in that list beyond the obvious NICs. any captures across every interface at once, which is the fastest way to answer "is this packet leaving the box at all." nflog and nfqueue let you capture packets that netfilter logged or queued, so you can see exactly what a firewall rule matched. And ciscodump and sshdump are extcap plugins that SSH into a remote device and stream a capture back, which means tshark can pull an EPC capture off a Cisco box without you ever copying a file.
The one concept that matters: capture filters against display filters
This trips up everyone who comes to tshark from tcpdump, because tshark has both and they use completely different syntax.
-f is a capture filter. It is BPF, the same language tcpdump uses, and it is applied in the kernel before the packet is ever copied to userspace. Packets it rejects are never seen, never counted and never written to your file.
-Y is a display filter. It is Wireshark's own language, it can reach into any field of any dissected protocol, and it runs after tshark has fully dissected the packet. Packets it rejects were still captured.
Here is that difference made visible. This run asked for six packets on port 80 with a display filter that only shows SYNs and HTTP requests, while curl fetched a page from WEB1:
j@llmbits:~$ sudo tshark -i ens224 -c 6 -f "tcp port 80" -Y "tcp.flags.syn==1 || http.request"
Capturing on 'ens224'
3 packets captured
1 0.000000000 10.77.0.100 → 10.77.2.10 TCP 74 51464 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=1259260457 TSecr=0 WS=128
2 0.004854831 10.77.2.10 → 10.77.0.100 TCP 74 80 → 51464 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=2214885239 TSecr=1259260457 WS=2048
4 0.005135906 10.77.0.100 → 10.77.2.10 HTTP 140 GET / HTTP/1.1Look at the frame numbers: 1, 2, 4. Frame 3 was captured, dissected and then hidden, because it was the bare ACK that completes the handshake and it matched neither half of the display filter. The gap in the numbering is the display filter's fingerprint. If you ever want to know whether a packet is missing because it was not on the wire or because your filter ate it, that gap is the answer.
The practical rule: use a capture filter to control what hits the disk, and a display filter to control what hits your eyes. On a busy link, a capture filter is the difference between a 200 MB file and a 40 GB file. A display filter cannot save you that, because the packet was already captured before -Y ran.
The -c gotcha that will waste an afternoon
-c counts captured packets, not displayed ones. Combine a small -c with a selective -Y and tshark will happily exit having shown you nothing at all:
j@llmbits:~$ sudo tshark -i ens224 -c 3 -f "tcp port 80" -Y "http.request"
Capturing on 'ens224'
0 packets captured
j@llmbits:~$ sudo tshark -i ens224 -c 3 -f "tcp port 80"
Capturing on 'ens224'
3 packets captured
1 0.000000000 10.77.0.100 → 10.77.2.10 TCP 74 52908 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=2248582813 TSecr=0 WS=128
2 0.004532634 10.77.2.10 → 10.77.0.100 TCP 74 80 → 52908 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=2214910466 TSecr=2248582813 WS=2048
3 0.004615864 10.77.0.100 → 10.77.2.10 TCP 66 52908 → 80 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=2248582818 TSecr=2214910466The three packets -c 3 consumed were the handshake. The GET was the fourth packet and tshark had already exited. If you want N matching packets, either tighten the capture filter so only interesting packets are captured, or drop -c and use -a duration:N instead.
Turning packets into columns with -T fields
This is the feature that makes tshark worth installing even if you never use it interactively. -T fields plus a list of -e field names produces tab-separated output that you can pipe into awk, sort, or a spreadsheet:
j@llmbits:~$ sudo tshark -i ens224 -c 8 -f "tcp port 80" \
-T fields -e frame.number -e ip.src -e ip.dst -e tcp.srcport \
-e tcp.dstport -e tcp.flags.str -e tcp.len -E header=y
frame.number ip.src ip.dst tcp.srcport tcp.dstport tcp.flags.str tcp.len
1 10.77.0.100 10.77.2.10 51468 80 ··········S· 0
2 10.77.2.10 10.77.0.100 80 51468 ·······A··S· 0
3 10.77.0.100 10.77.2.10 51468 80 ·······A···· 0
4 10.77.0.100 10.77.2.10 51468 80 ·······AP··· 74
5 10.77.2.10 10.77.0.100 80 51468 ·······A···· 0
6 10.77.2.10 10.77.0.100 80 51468 ·······AP··· 238
7 10.77.0.100 10.77.2.10 51468 80 ·······A···· 0
8 10.77.2.10 10.77.0.100 80 51468 ·······AP··· 896That is a complete HTTP transaction in eight lines: SYN, SYN-ACK, ACK, the 74-byte GET, the ACK for it, then a 238-byte response header followed by an 896-byte body. The tcp.flags.str field renders the flags positionally, which reads oddly at first but is very easy to grep.
Any field Wireshark can display, you can extract. -e http.host, -e dns.qry.name, -e tls.handshake.extensions_server_name, -e ip.ttl. To find a field name, run tshark -G fields and grep, or click the field in the Wireshark GUI once and read the status bar.
Writing and reading files
-w writes a pcapng, -r reads one back. Here is a nine second capture taken while iperf3 pushed traffic to SRV1:
j@llmbits:~$ sudo tshark -i ens224 -f "host 10.77.3.10" -w iperf.pcapng -a duration:9
Capturing on 'ens224'
11670 packets captured
j@llmbits:~$ ls -lh iperf.pcapng
-rw------- 1 root root 18M Aug 19 09:12 iperf.pcapngThe permission error that makes no sense
The first time I ran that, in a directory owned by my own user, tshark failed:
tshark: The file to which the capture would be saved ("iperf.pcapng") could not be
opened: Permission denied.
0 packets capturedIt was running as root at the time, in a directory root could obviously write. The reason is that tshark does not capture packets itself: it forks dumpcap, and dumpcap drops privileges before it opens the output file. The child that creates the file is not the user you think it is. The fix is either to write somewhere the unprivileged capture user can create files, or to run dumpcap directly and let tshark read the result afterward. This is worth knowing before you lose a capture window to it.
Ring buffers for captures that run for days
If you are chasing something intermittent, you cannot leave -w running for a week. -b gives you a rotating set of files:
j@llmbits:~$ sudo tshark -i ens224 -f "host 10.77.3.10" -w ring.pcapng \
-b filesize:200 -b files:3 -a duration:8
Capturing on 'ens224'
9681 packets captured
j@llmbits:~$ ls -l ring*.pcapng
-rw------- 1 root root 200044 Aug 19 09:12 ring_00070_20260819091246.pcapng
-rw------- 1 root root 200692 Aug 19 09:12 ring_00071_20260819091246.pcapng
-rw------- 1 root root 44968 Aug 19 09:12 ring_00072_20260819091246.pcapngRead the file numbers. tshark wrote seventy two files during those eight seconds and kept three. filesize is in kilobytes, so 200 means a 200 KB file, which under an iperf3 load rotates roughly ten times a second. In production you would use something like -b filesize:100000 -b files:20, giving a 2 GB rolling window. The point is that disk usage is bounded and the newest data always survives.
The statistics engine
This is where tshark leaves tcpdump behind entirely. -z runs one of Wireshark's statistics taps and prints the result when the capture ends. Pair it with -q to suppress the per-packet lines.
Who is talking to whom
j@llmbits:~$ tshark -r iperf.pcapng -q -z conv,tcp
================================================================================
TCP Conversations
Filter:<No Filter>
| <- | | -> | | Total | Relative | Duration |
| Frames Bytes | | Frames Bytes | | Frames Bytes | Start | |
10.77.0.100:56962 <-> 10.77.3.10:5201 6195 413 kB 5445 17 MB 11640 17 MB 0.016673143 5.0273
10.77.0.100:56960 <-> 10.77.3.10:5201 14 1,269 bytes 16 1,561 bytes 30 2,830 bytes 0.000000000 5.0472
================================================================================Two conversations, not one. Port 56960 carried 30 packets and about 2.8 KB: that is iperf3's control connection, which negotiates the test and reports results. Port 56962 carried 17 MB: that is the actual data stream. If you have ever wondered why an iperf3 test opens two sockets, there it is on the wire.
Throughput over time
j@llmbits:~$ tshark -r iperf.pcapng -q -z io,stat,1
===============================
| IO Statistics |
| |
| Duration: 5. 47156 secs |
| Interval: 1 secs |
| |
| Col 1: Frames and bytes |
|-----------------------------|
| |1 |
| Interval | Frames | Bytes |
|-----------------------------|
| 0 <> 1 | 1729 | 2473887 |
| 1 <> 2 | 2079 | 3054518 |
| 2 <> 3 | 2424 | 3529040 |
| 3 <> 4 | 2572 | 3942920 |
| 4 <> 5 | 2729 | 4393538 |
| 5 <> Dur| 137 | 170316 |
===============================That is TCP slow start rendered as a table: 2.47 MB in the first second climbing to 4.39 MB in the fifth as the congestion window opens. You can add a filter to any column, so -z io,stat,1,"tcp.analysis.retransmission" gives you a per-second retransmission count, which is exactly the graph you want when someone says "the network was slow around 3pm."
What went wrong, summarized
j@llmbits:~$ tshark -r iperf.pcapng -q -z expert
Warns (20)
=============
Frequency Group Protocol Summary
20 Sequence TCP Connection reset (RST)
Notes (2060)
=============
Frequency Group Protocol Summary
1583 Sequence TCP Partial Acknowledgement of a segment
29 Sequence TCP Duplicate ACK (#1)
29 Sequence TCP Duplicate ACK (#2)
42 Sequence TCP This frame is a (suspected) fast retransmission
52 Sequence TCP This frame is a (suspected) retransmission
29 Sequence TCP Duplicate ACK (#3)
28 Sequence TCP Duplicate ACK (#4)The expert info tap is the single highest-value thing in tshark for triage. It reads a capture you know nothing about and tells you what Wireshark thinks is abnormal, in one screen.
A worked example: corroborating a retransmit count
Here is why this matters in practice. iperf3 reported its own retransmission count for the transfer:
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-5.00 sec 16.1 MBytes 27.0 Mbits/sec 53 sender
[ 5] 0.00-5.01 sec 15.9 MBytes 26.6 Mbits/sec receiverFifty three retransmits, according to the kernel's own counters on the sending host. Now ask the wire:
j@llmbits:~$ tshark -r iperf.pcapng -Y "tcp.analysis.retransmission" | wc -l
52Fifty two. Those two numbers come from completely independent sources: one is tcpi_total_retrans read out of the kernel by iperf3, the other is Wireshark's sequence-number analysis of packets captured off the NIC. They agree to within one packet, and the one packet is easily explained by the capture starting a fraction of a second after the transfer did.
That agreement is the whole point. When a host-level counter and an independent packet capture tell the same story, you can stop arguing about whether the tool is lying and start fixing the path. When they disagree badly, that itself is the finding: it usually means offloads are hiding the real segmentation from your capture, which is covered in ethtool: link speed, duplex, offloads and NIC stats.
Following a stream and reading a full dissection
-z follow,tcp,ascii,0 reassembles stream 0 and prints it as text, which is the terminal equivalent of Wireshark's Follow TCP Stream:
j@llmbits:~$ tshark -r http.pcapng -q -z follow,tcp,ascii,0
Follow: tcp,ascii
Filter: tcp.stream eq 0
Node 0: 10.77.0.100:54370
Node 1: 10.77.2.10:80
74
GET / HTTP/1.1
Host: 10.77.2.10
User-Agent: curl/8.14.1
Accept: */*
238
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:54:41 GMT
Content-Type: text/html
Content-Length: 896
Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT
Connection: keep-alive
ETag: "69d4ec68-380"
Accept-Ranges: bytes
896The numbers before each block are byte counts, and the indentation marks direction. When you need the opposite, maximum detail on a single packet rather than a whole conversation, -V prints the complete dissection tree:
j@llmbits:~$ tshark -r http.pcapng -Y "http.request" -V
Frame 4: 140 bytes on wire (1120 bits), 140 bytes captured (1120 bits) on interface ens224, id 0
Arrival Time: Aug 19, 2026 08:54:41.169864491 PDT
[Protocols in frame: eth:ethertype:ip:tcp:http]
Ethernet II, Src: VMware_b1:cc:47 (00:0c:29:b1:cc:47), Dst: aa:bb:cc:00:04:00 (aa:bb:cc:00:04:00)
Destination: aa:bb:cc:00:04:00 (aa:bb:cc:00:04:00)
.... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
Internet Protocol Version 4, Src: 10.77.0.100, Dst: 10.77.2.10
Total Length: 126
Identification: 0x08d2 (2258)
010. .... = Flags: 0x2, Don't fragment
Time to Live: 64
Protocol: TCP (6)Two details in that output are worth pausing on. The destination MAC is aa:bb:cc:00:04:00 and tshark flags the locally administered bit, because that is R1's interface and IOL allocates MACs from a locally administered pool rather than a vendor OUI. And the source resolves to VMware_b1:cc:47, because tshark looked up the OUI. If you would rather see raw values and skip every name lookup, -n disables all of them.
What tshark is bad at
Two honest limitations, because pretending otherwise leads people to the wrong tool.
It is slow. Full dissection costs CPU per packet. On a genuinely busy interface, tshark will drop packets that tcpdump would have caught, because tcpdump does almost nothing per packet. The correct pattern on a hot link is tcpdump -w to capture and tshark -r to analyze. Do not dissect at line rate if you can avoid it.
It cannot decrypt what it does not have keys for. tshark will tell you a TLS session exists, which cipher suite was negotiated and what server name the client asked for, and then it stops. Without the session keys it cannot show you the HTTP inside. That boundary, and what leaks anyway, is the subject of the companion article on ngrep: grep for network traffic.
Key takeaways
-fis a kernel-side capture filter in BPF syntax;-Yis a userspace display filter in Wireshark syntax. Only-fsaves you disk and CPU.- Gaps in the frame numbering are the display filter's signature. Frames 1, 2, 4 means frame 3 was captured and hidden, not missing from the wire.
-ccounts captured frames, not displayed ones. Pair a tight-Ywith-a duration:Ninstead.-T fields -e ...turns packets into tab-separated columns, which is the fastest path from a capture to a report.-b filesize:N -b files:Mbounds a long-running capture to a fixed amount of disk.-z expertis the best thirty seconds you can spend on a capture you did not take yourself.-z conv,tcpand-z io,stat,1answer "who" and "when."- tshark writing a file can fail with "Permission denied" even as root, because dumpcap drops privileges before creating it.
- Capture with tcpdump on busy links, analyze with tshark afterward. Full dissection at line rate drops packets.
tshark is the analysis half of a pair. The capture half is tcpdump, the pattern-matching half is ngrep, and the question of how much traffic is even moving belongs to the live bandwidth monitors. All of them, along with the rest of the toolkit, are indexed in the complete guide to Linux networking commands.