Loading content...
At the core of every OpenFlow switch lies a deceptively simple abstraction: the flow table. This is where high-level network policies—routing decisions, access control rules, quality of service classifications—transform into concrete packet handling behavior.
A flow table is fundamentally a lookup table that answers the question: "What should I do with this packet?" Every packet entering an OpenFlow switch traverses one or more flow tables, where it is matched against stored entries and subjected to the actions those entries prescribe.
Yet this apparent simplicity conceals profound depth. Flow tables must support wildcard matching across dozens of header fields. They must prioritize among overlapping entries. They must track statistics per-flow. They must support modification while handling millions of packets per second. And modern OpenFlow switches chain multiple tables into processing pipelines that rival the complexity of full programming languages.
This page provides an exhaustive exploration of OpenFlow flow tables: their structure, their entries, their pipeline organization, and the engineering considerations that govern their capacity and performance. Understanding flow tables is essential—they are the translation layer between SDN intelligence and actual packet forwarding.
By completing this page, you will understand: the complete structure of flow table entries, how priority-based matching resolves overlaps, the multi-table pipeline introduced in OpenFlow 1.1+, table-miss handling and default behaviors, hardware implementation via TCAMs, capacity constraints and their implications, and best practices for efficient table utilization.
The Conceptual Model
A flow table is an ordered collection of flow entries, where each entry specifies:
When a packet arrives, the switch examines all entries in the table, identifies those whose match fields align with the packet's headers, selects the highest-priority matching entry, and executes its instructions.
Priority-Based Matching
Priority is the tiebreaker when packets match multiple entries. OpenFlow priorities range from 0 (lowest) to 65535 (highest). When a packet matches multiple entries, only the highest-priority entry's instructions execute.
This enables powerful policy layering:
Counter Tracking
Every flow entry maintains counters updated atomically as packets match:
| Counter | Description | Notes |
|---|---|---|
| packet_count | Total packets matching this entry | 64-bit, monotonically increasing |
| byte_count | Total bytes matching this entry | 64-bit, includes headers |
| duration_sec | Seconds since flow installed | For calculating rates |
| duration_nsec | Nanoseconds beyond duration_sec | High-precision timing |
Controllers can query counters via MULTIPART_REQUEST (stats), but frequent polling creates overhead. OpenFlow 1.4+ allows push-based flow monitoring where controllers subscribe to counter updates. For high-frequency monitoring, consider dedicated collector infrastructure that aggregates switch statistics.
Let's examine the complete structure of a flow entry, understanding each component at the implementation level.
The Complete Flow Entry
1234567891011121314151617181920212223242526272829303132
/* Conceptual flow entry structure * (actual OpenFlow encoding is more complex) */struct flow_entry { /* === Identification === */ uint64_t cookie; /* Controller-assigned identifier */ uint64_t cookie_mask; /* For matching in modify/delete operations */ /* === Matching === */ uint16_t priority; /* 0-65535, higher wins on overlap */ struct ofp_match match; /* OXM TLV match fields */ /* === Timeouts === */ uint16_t idle_timeout; /* Seconds without match before expiry */ uint16_t hard_timeout; /* Absolute seconds until expiry */ /* === Flags === */ uint16_t flags; /* OFPFF_SEND_FLOW_REM: notify controller on expiry * OFPFF_CHECK_OVERLAP: fail if would create ambiguity * OFPFF_RESET_COUNTS: reset counters on modify * OFPFF_NO_PKT_COUNTS: don't count packets * OFPFF_NO_BYT_COUNTS: don't count bytes */ /* === Processing === */ struct ofp_instruction instructions[]; /* What to do with matched packets */ /* === Statistics (maintained by switch) === */ uint64_t packet_count; /* Packets matched */ uint64_t byte_count; /* Bytes matched */ uint32_t duration_sec; /* Time since installation (seconds) */ uint32_t duration_nsec; /* Time since installation (nanoseconds) */};Cookie Management
The 64-bit cookie field is a controller-opaque identifier—the switch doesn't interpret it, merely stores and reports it. Cookies enable:
The cookie_mask enables partial matching: if cookie_mask is 0xFFFF000000000000, only the top 16 bits of the cookie are compared during modify/delete operations.
Structure your cookies systematically. Common patterns: (1) Top bits = application ID, (2) Middle bits = tenant/customer ID, (3) Low bits = flow sequence number. This enables efficient bulk operations: delete all flows for tenant X by matching cookie with appropriate mask.
Timeout Mechanisms
Flow entries support two independent timeout mechanisms:
Idle Timeout: The entry expires if it hasn't matched any packets for this many seconds. Useful for entries that should persist only while traffic is active—once a connection completes, the entry automatically disappears.
Hard Timeout: The entry expires unconditionally after this many seconds, regardless of activity. Useful for time-limited policies, session tokens, or periodic refresh requirements.
Both timeouts can be set to 0, meaning "never expire"—permanent entries that persist until explicitly deleted.
Entry Flags
The flags field controls entry behavior:
| Flag | Effect |
|---|---|
| OFPFF_SEND_FLOW_REM | Switch sends FLOW_REMOVED message when entry expires |
| OFPFF_CHECK_OVERLAP | Installation fails if an overlapping entry exists with same priority |
| OFPFF_RESET_COUNTS | Reset packet/byte counters when modifying entry |
| OFPFF_NO_PKT_COUNTS | Don't maintain packet counter (saves resources) |
| OFPFF_NO_BYT_COUNTS | Don't maintain byte counter (saves resources) |
The CHECK_OVERLAP flag deserves special attention. By default, overlapping entries are allowed—priority determines the winner. But if CHECK_OVERLAP is set and the switch detects that a new entry would overlap with an existing same-priority entry (where the winner is ambiguous), it returns an error instead of installing.
OpenFlow match fields specify which packets an entry applies to. The protocol has evolved from a fixed 12-tuple (OF 1.0) to an extensible Type-Length-Value (TLV) format called OXM (OpenFlow Extensible Match) in OF 1.2+.
Layer 2 (Data Link) Match Fields
| Field | Size | Description | Maskable? |
|---|---|---|---|
| in_port | 32 bits | Physical or logical switch port packet arrived on | No |
| in_phy_port | 32 bits | Physical port (when in_port is a tunnel endpoint) | No |
| metadata | 64 bits | Controller-written metadata passed between tables | Yes |
| eth_dst | 48 bits | Ethernet destination MAC address | Yes |
| eth_src | 48 bits | Ethernet source MAC address | Yes |
| eth_type | 16 bits | Ethernet type (0x0800=IPv4, 0x86DD=IPv6, etc.) | No |
| vlan_vid | 12 bits | VLAN ID from 802.1Q header | Yes |
| vlan_pcp | 3 bits | VLAN priority code point | No |
Layer 3 (Network) Match Fields
| Field | Size | Description | Maskable? |
|---|---|---|---|
| ip_dscp | 6 bits | Differentiated Services Code Point | No |
| ip_ecn | 2 bits | Explicit Congestion Notification bits | No |
| ip_proto | 8 bits | IP protocol number (6=TCP, 17=UDP, 1=ICMP) | No |
| ipv4_src | 32 bits | IPv4 source address | Yes (prefix) |
| ipv4_dst | 32 bits | IPv4 destination address | Yes (prefix) |
| ipv6_src | 128 bits | IPv6 source address | Yes (prefix) |
| ipv6_dst | 128 bits | IPv6 destination address | Yes (prefix) |
| ipv6_flabel | 20 bits | IPv6 flow label | Yes |
| ipv6_exthdr | 9 bits | IPv6 extension header pseudo-field | Yes |
Layer 4 (Transport) Match Fields
| Field | Size | Description | Maskable? |
|---|---|---|---|
| tcp_src | 16 bits | TCP source port | No |
| tcp_dst | 16 bits | TCP destination port | No |
| tcp_flags | 12 bits | TCP flags (SYN, ACK, FIN, etc.) | Yes |
| udp_src | 16 bits | UDP source port | No |
| udp_dst | 16 bits | UDP destination port | No |
| sctp_src | 16 bits | SCTP source port | No |
| sctp_dst | 16 bits | SCTP destination port | No |
| icmpv4_type | 8 bits | ICMPv4 message type | No |
| icmpv4_code | 8 bits | ICMPv4 message code | No |
| icmpv6_type | 8 bits | ICMPv6 message type | No |
| icmpv6_code | 8 bits | ICMPv6 message code | No |
MPLS and Tunnel Match Fields
| Field | Size | Description | Maskable? |
|---|---|---|---|
| mpls_label | 20 bits | MPLS label value | No |
| mpls_tc | 3 bits | MPLS traffic class | No |
| mpls_bos | 1 bit | Bottom-of-Stack bit | No |
| pbb_isid | 24 bits | Provider Backbone Bridge I-SID | Yes |
| tunnel_id | 64 bits | Logical port metadata (tunnel endpoint) | Yes |
Most match fields have prerequisites—you can only match on tcp_dst if you've also specified ip_proto=6 (TCP). OXM enforces this through explicit preconditions. Common chains: eth_type=0x0800 → ipv4_* → tcp_* or eth_type=0x8847 → mpls_*. Failing to specify prerequisites results in flow installation errors.
Wildcarding and Masking
OpenFlow supports two levels of matching granularity:
Exact match: The packet header field must exactly equal the specified value. Used for specific host/port matches.
Wildcard match: The field is either fully wildcarded (any value matches) or masked (certain bits must match). IP addresses commonly use CIDR-style prefix matching implemented via masks.
Example: To match all packets from 10.0.0.0/8:
Wildcards dramatically reduce table entries needed. Instead of 256 entries for 10.0.0.0 through 10.0.0.255, a single wildcarded entry covers all.
OpenFlow 1.0 supported only a single flow table—functional but limiting. Complex forwarding decisions required cramming everything into one table, leading to explosion in entry count due to policy Cartesian products.
OpenFlow 1.1 introduced multiple flow tables arranged in a processing pipeline. Packets traverse tables sequentially, with each table adding context, making decisions, or accumulating actions for eventual execution.
Pipeline Processing Model
Key Pipeline Concepts
Table numbering: Tables are numbered 0 through n-1. Processing always starts at table 0. Entries can only forward to higher-numbered tables (no backward goto).
Metadata: A 64-bit register passed between tables. Tables can write metadata that influences decisions in later tables. Example: Table 0 classifies traffic as "external" (metadata bit) → Table 2 applies stricter routing for external traffic.
Action set: Actions accumulated across tables, executed atomically when packet exits the pipeline. Multiple tables can contribute to the final action set.
Goto-table instruction: Directs the packet to continue processing at a specified table. If no goto-table instruction is present in the matched entry, the packet exits the pipeline and the action set executes.
Design pipelines to mirror conceptual processing stages. A common pattern: Table 0 (port/VLAN classification) → Table 1 (L2 learning/forwarding) → Table 2 (L3 routing) → Table 3 (security/ACL) → Table 4 (QoS) → Table 5 (output). This makes policies readable and maintainable.
Action Set vs. Apply-Actions
OpenFlow distinguishes between two ways to execute actions:
Apply-actions instruction: Execute actions immediately, in order, then continue processing. Useful for packet modifications that affect subsequent table matching.
Write-actions instruction: Add actions to the action set without executing yet. Actions accumulate across tables and execute atomically when the packet exits the pipeline.
Clear-actions instruction: Remove all actions from the action set. Enables one table to override earlier decisions.
When the packet exits the pipeline, the action set executes in a defined order:
This ordering ensures predictable behavior regardless of the order actions were added to the set.
What happens when a packet doesn't match any entry in a flow table? This table-miss scenario is fundamental to OpenFlow operation and has evolved across protocol versions.
Table-Miss Entry (Modern Approach)
In OpenFlow 1.3+, table-miss is handled by a special flow entry:
This entry is treated like any other flow entry—it has counters, can be modified, and can be deleted. If no table-miss entry exists and no other entries match, the packet is dropped by default.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
# Table-miss: Send to controller# Used for reactive flow installationdef add_table_miss_entry_to_controller(self, datapath, table_id=0): ofproto = datapath.ofproto parser = datapath.ofproto_parser # Match: empty (matches everything) match = parser.OFPMatch() # Actions: send packet to controller actions = [parser.OFPActionOutput( ofproto.OFPP_CONTROLLER, ofproto.OFPCML_NO_BUFFER # Send full packet )] # Instructions: apply actions immediately instructions = [parser.OFPInstructionActions( ofproto.OFPIT_APPLY_ACTIONS, actions)] # Install with priority 0 (table-miss) mod = parser.OFPFlowMod( datapath=datapath, table_id=table_id, priority=0, # Lowest priority match=match, instructions=instructions ) datapath.send_msg(mod) # Table-miss: Drop# Secure default for production networksdef add_table_miss_entry_drop(self, datapath, table_id=0): ofproto = datapath.ofproto parser = datapath.ofproto_parser match = parser.OFPMatch() instructions = [] # No instructions = drop mod = parser.OFPFlowMod( datapath=datapath, table_id=table_id, priority=0, match=match, instructions=instructions ) datapath.send_msg(mod) # Table-miss: Forward to next table# Used in multi-table pipelinesdef add_table_miss_entry_goto(self, datapath, src_table, dst_table): ofproto = datapath.ofproto parser = datapath.ofproto_parser match = parser.OFPMatch() # Instruction: goto next table instructions = [parser.OFPInstructionGotoTable(dst_table)] mod = parser.OFPFlowMod( datapath=datapath, table_id=src_table, priority=0, match=match, instructions=instructions ) datapath.send_msg(mod)Sending every unmatched packet to the controller creates significant overhead. At 10 Gbps line rate with 64-byte packets, a table-miss rate of just 1% generates 1.5 million PACKET_IN messages per second. Controllers must handle this load or networks collapse. Strategies: aggressive proactive flow installation, flow aggregation with wildcards, and careful table-miss policy selection.
Historical: OF 1.0 Table-Miss Behavior
In OpenFlow 1.0, table-miss behavior was configured via SET_CONFIG message with the 'send packet to controller' length. If miss_send_len was 0, unmatched packets were dropped. Otherwise, the first miss_send_len bytes were sent to the controller.
This legacy mechanism was replaced by the explicit table-miss entry approach in OF 1.3, which provides more flexible handling and is consistent with normal flow entry management.
Understanding flow table hardware implementation is essential for designing efficient SDN applications. The key technology is TCAM (Ternary Content-Addressable Memory).
Content-Addressable Memory (CAM)
Traditional memory is address-addressable: you provide an address, it returns data at that address. CAM inverts this: you provide data (search key), and it returns the address where that data is stored (if anywhere).
This enables O(1) exact-match lookups regardless of table size—essential for wire-speed packet processing.
Ternary CAM (TCAM)
TCAM extends CAM with a third state: "don't care" (X). Each bit can be:
This enables wildcard matching. An IPv4 address entry of "10.0.X.X" (where each X represents 8 don't-care bits) matches all addresses in 10.0.0.0/16.
TCAM Characteristics and Constraints
TCAM is expensive—both in cost and power:
| Aspect | TCAM | SRAM |
|---|---|---|
| Lookup speed | O(1) parallel, ~10ns | O(1) indexed, ~10ns |
| Match type | Wildcards, masks, ranges | Exact match only |
| Power consumption | ~15W per Mbit | ~0.1W per Mbit |
| Cost per bit | ~10x SRAM | Baseline |
| Density | ~6 transistors/bit | ~1 transistor/bit |
| Typical capacity | 1K-64K entries | Millions of entries |
A typical top-of-rack switch might have 2K-8K TCAM entries for OpenFlow. Each wildcard flow entry consumes one TCAM slot. Running out of TCAM means new flows are rejected. Smart entry design—using wildcards, aggregating flows, minimizing match fields—is essential for scalable SDN deployment.
Entry Width and Utilization
TCAM entries have fixed width. A commodity switch TCAM might be 480 bits wide to accommodate all OpenFlow match fields. If your match only uses 48+48 (MAC) + 32 (IPv4 dst) = 128 bits, you still consume the full 480-bit entry.
Strategies to maximize TCAM utilization:
Field reduction: Only include match fields you actually need. Platform-specific implementations may optimize based on which fields are populated.
Entry aggregation: Combine multiple specific entries into wildcarded supersets where possible. 10.0.0.1, 10.0.0.2, ... 10.0.0.255 → 10.0.0.0/24.
Table migration: Move exact-match flows to cheaper SRAM hash tables. Reserve TCAM for wildcard matches. Some switches support hybrid table modes.
Priority encoding: TCAM inherently costs more for lower priorities that might match (must check all entries). Some architectures optimize by partitioning.
Software Flow Tables
Not all OpenFlow implementations use hardware TCAM. Software switches (Open vSwitch, BESS) implement flow tables in CPU memory using optimized data structures:
Linear search: Simple but O(n). Only viable for very small tables.
Hash tables: O(1) average for exact match. Doesn't support wildcards.
Tuple space search: Group entries by "tuple" (set of fields matched with what mask). Hash within tuples. Enables wildcard matching with reasonable performance.
Decision trees: Hierarchical structure optimized for specific field combinations.
Software switches trade performance for flexibility—Open vSwitch can handle millions of entries but at lower packet rates than hardware switches.
Planning flow table capacity is a critical network engineering exercise. Running out of table space mid-deployment is catastrophic—new connections may fail silently or fall through to unintended catch-all rules.
Capacity Factors
Estimating Entry Requirements
Entry requirements depend on your application model:
| Application | Entry Scaling Factor | Example at 1000 hosts |
|---|---|---|
| L2 MAC learning | O(n) hosts | 1,000 entries |
| L3 routing (per-host) | O(n) hosts | 1,000 entries |
| L3 routing (prefixes) | O(p) prefixes | ~600K Internet routes (problem!) |
| Firewall (host pairs) | O(n²) forbidden pairs | 1,000,000 entries (problem!) |
| Load balancing (VIPs) | O(v) virtual IPs | Typically 10-100 entries |
| QoS classification | O(c) traffic classes | Typically 10-50 entries |
| Reactive per-flow | O(f) active flows | Variable—could be millions |
Policies that scale with the square of entities (host-to-host ACLs, all-pairs QoS) quickly exhaust tables. Redesign to use aggregation (subnet policies), indirection (group-based tagging), or push evaluation to endpoint hosts where per-host state is acceptable.
Capacity Monitoring
OpenFlow provides mechanisms to monitor table utilization:
Multipart Table Stats Request: Query entry counts per table Table Features Request: Query maximum table capacities Vacancy Events (OF 1.4+): Proactive notification when table reaches threshold
Production deployments should:
Failure Modes
When tables fill, switches typically reject new FLOW_MOD installations with an error. Some scenarios:
Graceful degradation: New flows route through table-miss (may overload controller) Silent failure: Packets drop or route incorrectly Cascading failure: Back-pressure to controller stalls entire network
Design for headroom—plan for 50-70% peak utilization to accommodate bursts.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
from ryu.controller.handler import set_ev_clsfrom ryu.controller import ofp_event class TableMonitor: """Monitor flow table utilization and alert on capacity issues.""" WARNING_THRESHOLD = 0.70 # 70% CRITICAL_THRESHOLD = 0.85 # 85% def request_table_stats(self, datapath): """Request current table statistics from switch.""" parser = datapath.ofproto_parser req = parser.OFPTableStatsRequest(datapath, 0) datapath.send_msg(req) @set_ev_cls(ofp_event.EventOFPTableStatsReply, MAIN_DISPATCHER) def table_stats_reply_handler(self, ev): """Process table statistics reply.""" datapath = ev.msg.datapath for stat in ev.msg.body: table_id = stat.table_id active_count = stat.active_count max_entries = stat.max_entries # May be 0 if unknown if max_entries > 0: utilization = active_count / max_entries if utilization >= self.CRITICAL_THRESHOLD: self.logger.critical( f"CRITICAL: Switch {datapath.id} Table {table_id} " f"at {utilization:.1%} capacity " f"({active_count}/{max_entries})" ) self.trigger_emergency_cleanup(datapath, table_id) elif utilization >= self.WARNING_THRESHOLD: self.logger.warning( f"WARNING: Switch {datapath.id} Table {table_id} " f"at {utilization:.1%} capacity" ) def trigger_emergency_cleanup(self, datapath, table_id): """Emergency flow cleanup when table near capacity.""" # Example: Delete oldest idle flows parser = datapath.ofproto_parser ofproto = datapath.ofproto # Request flow stats sorted by idle time match = parser.OFPMatch() req = parser.OFPFlowStatsRequest( datapath, 0, table_id, ofproto.OFPP_ANY, ofproto.OFPG_ANY, 0, 0, match ) datapath.send_msg(req) # Handler would identify and delete least-recently-matched flowsFlow tables are the foundation of OpenFlow packet processing. Every forwarding decision, policy enforcement, and traffic manipulation is expressed through flow entries that match packets and execute actions.
What's Next:
With flow table fundamentals understood, we'll now explore match-action rules in detail—the combining of match fields with specific actions to express complete forwarding policies. You'll learn the full action vocabulary, instruction types, and how to compose them for complex network behaviors.
You now have a thorough understanding of OpenFlow flow tables—their structure, matching semantics, pipeline organization, and hardware constraints. This knowledge is essential for designing efficient SDN forwarding rules and capacity planning. Next, we dive deep into match-action rules.