diff --git a/example/Makefile.am b/example/Makefile.am
index 7f82c4d..ab37f21 100644
--- a/example/Makefile.am
+++ b/example/Makefile.am
@@ -1 +1 @@
-SUBDIRS = classifier generator ipsec packet time timer traffic_mgmt l2fwd_simple switch
+SUBDIRS = classifier generator ipsec packet time timer traffic_mgmt l2fwd_simple switch echo
diff --git a/example/echo/.gitignore b/example/echo/.gitignore
new file mode 100644
index 0000000..b1e5eb5
--- /dev/null
+++ b/example/echo/.gitignore
@@ -0,0 +1 @@
+odp_echo
diff --git a/example/echo/Makefile.am b/example/echo/Makefile.am
new file mode 100644
index 0000000..323522f
--- /dev/null
+++ b/example/echo/Makefile.am
@@ -0,0 +1,10 @@
+include $(top_srcdir)/example/Makefile.inc
+
+bin_PROGRAMS = odp_echo$(EXEEXT)
+odp_echo_LDFLAGS = $(AM_LDFLAGS) -static
+odp_echo_CFLAGS = $(AM_CFLAGS) -I${top_srcdir}/example
+
+noinst_HEADERS = \
+		  $(top_srcdir)/example/example_debug.h
+
+dist_odp_echo_SOURCES = odp_echo.c
diff --git a/example/echo/odp_echo.c b/example/echo/odp_echo.c
new file mode 100644
index 0000000..cc966e1
--- /dev/null
+++ b/example/echo/odp_echo.c
@@ -0,0 +1,528 @@
+/* Copyright (c) 2016, Linaro Limited
+ * All rights reserved.
+ *
+ * SPDX-License-Identifier:     BSD-3-Clause
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <getopt.h>
+
+#include <odp_api.h>
+#include <odp/helper/linux.h>
+#include <odp/helper/eth.h>
+#include <odp/helper/ip.h>
+
+#define POOL_NUM_PKT 256
+#define POOL_SEG_LEN 1856
+
+#define MAX_WORKERS 2
+
+#define ECHO_PACKET_LEN 64
+#define ECHO_MAGIC 0x92749451
+#define ECHO_TX_INTERVAL ODP_TIME_SEC_IN_NS
+
+/**
+ * Application mode
+ */
+typedef enum appl_mode_t {
+	TRANSMIT,
+	RESPOND
+} appl_mode_t;
+
+/** Get rid of path in filename - only for unix-type paths using '/' */
+#define NO_PATH(file_name) (strrchr((file_name), '/') ? \
+			    strrchr((file_name), '/') + 1 : (file_name))
+
+/**
+ * Parsed command line application arguments
+ */
+typedef struct {
+	char *if_str;		/**< Storage for interface name */
+	odph_ethaddr_t dst;	/**< Destination MAC address */
+	appl_mode_t mode;	/**< Application mode */
+	uint64_t wait;		/**< Wait new packets */
+	int burst_size;		/**< Packet burst size */
+} appl_args_t;
+
+/**
+ * Global application data
+ */
+struct {
+	appl_args_t appl;	   /**< Parsed application arguments */
+	odp_pktio_t iface;	   /**< Pktio handle */
+	odph_ethaddr_t src;	   /**< Source MAC address */
+	odp_pktin_queue_t pktin;   /**< Packet input queue */
+	odp_pktout_queue_t pktout; /**< packet output queue */
+	odp_pool_t pool;	   /**< Packet pool */
+} global;
+
+/**
+ * Echo packet header
+ */
+typedef struct ODP_PACKED {
+	uint32_t llc;		/**< LLC header */
+	uint32_t magic;		/**< Magic number for verifying echo packets */
+	uint32_t seq;		/**< Sequence number */
+	uint64_t ts;		/**< Timestamp (ns) */
+} echo_hdr_t;
+
+/**
+ * Create a pktio handle
+ *
+ * @param dev         Device name
+ * @param pool        Associated Packet Pool
+ * @param pktin[out]  Packet input queue
+ * @param pktout[out] Packet output queue
+ *
+ * @return The handle of the created pktio object.
+ */
+static odp_pktio_t create_pktio(const char *name, odp_pool_t pool,
+				odp_pktin_queue_t *pktin,
+				odp_pktout_queue_t *pktout)
+{
+	odp_pktio_param_t pktio_param;
+	odp_pktin_queue_param_t in_queue_param;
+	odp_pktout_queue_param_t out_queue_param;
+	odp_pktio_t pktio;
+
+	odp_pktio_param_init(&pktio_param);
+
+	pktio = odp_pktio_open(name, pool, &pktio_param);
+	if (pktio == ODP_PKTIO_INVALID) {
+		printf("Error: failed to open %s\n", name);
+		exit(1);
+	}
+	/* Configure one input queue */
+	odp_pktin_queue_param_init(&in_queue_param);
+	in_queue_param.op_mode = ODP_PKTIO_OP_MT_UNSAFE;
+	in_queue_param.num_queues = 1;
+	if (odp_pktin_queue_config(pktio, &in_queue_param)) {
+		printf("Error: failed to config input queue for %s\n", name);
+		exit(1);
+	}
+	/* Configure one output queue */
+	odp_pktout_queue_param_init(&out_queue_param);
+	out_queue_param.op_mode = ODP_PKTIO_OP_MT_UNSAFE;
+	out_queue_param.num_queues = 1;
+	if (odp_pktout_queue_config(pktio, &out_queue_param)) {
+		printf("Error: failed to config output queue for %s\n", name);
+		exit(1);
+	}
+	/* Save input and output queue handles */
+	if (odp_pktin_queue(pktio, pktin, 1) != 1) {
+		printf("Error: pktin queue query failed for %s\n", name);
+		exit(1);
+	}
+	if (odp_pktout_queue(pktio, pktout, 1) != 1) {
+		printf("Error: pktout queue query failed for %s\n", name);
+		exit(1);
+	}
+	return pktio;
+}
+
+/**
+ * Initialize echo packet headers
+ *
+ * @param pkt  Packet handle
+ *
+ * @retval 0 on success
+ * @retval <0 on failure
+ */
+static uint32_t echo_packet_init(odp_packet_t pkt)
+{
+	odph_ethhdr_t *eth;
+	echo_hdr_t echo;
+	static uint32_t seq_num;
+
+	/* Ethernet headers */
+	eth = odp_packet_data(pkt);
+	eth->src = global.src;
+	eth->dst = global.appl.dst;
+	eth->type = odp_cpu_to_be_16(odp_packet_len(pkt) - ODPH_ETHHDR_LEN);
+
+	/* Echo headers */
+	echo.llc = 0; /* NULL DSAP, SSAP and control fields */
+	echo.magic = ECHO_MAGIC;
+	echo.seq = seq_num++;
+	echo.ts = odp_time_to_ns(odp_time_global());
+
+	if (odp_packet_copy_from_mem(pkt, ODPH_ETHHDR_LEN, sizeof(echo),
+				     &echo) != 0) {
+		printf("Error: copying echo header failed\n");
+		return -1;
+	}
+	return 0;
+}
+
+/**
+ * Echo header start pointer
+ *
+ * @param pkt  Packet handle
+ *
+ * @return  Echo header start pointer
+ * @retval  NULL packet does not contain a valid echo header
+ */
+static echo_hdr_t *echo_hdr(odp_packet_t pkt)
+{
+	odph_ethhdr_t *eth;
+	echo_hdr_t *echo;
+
+	eth = odp_packet_l2_ptr(pkt, NULL);
+	echo = (echo_hdr_t *)(eth + 1);
+
+	if (echo->magic != ECHO_MAGIC)
+		return NULL;
+
+	return echo;
+}
+
+/**
+ * Echo packet responder thread
+ */
+static int run_responder(void *arg ODP_UNUSED)
+{
+	odp_packet_t pkt_tbl[global.appl.burst_size];
+	odp_packet_t tx_tbl[global.appl.burst_size];
+	int pkts, sent, tx_drops, i;
+
+	printf("\n*** Echo responder started ***\n\n");
+
+	for (;;) {
+		int num_rx = 0;
+
+		pkts = odp_pktin_recv_tmo(global.pktin, pkt_tbl,
+					  global.appl.burst_size,
+					  global.appl.wait);
+
+		if (odp_unlikely(pkts <= 0))
+			continue;
+		for (i = 0; i < pkts; i++) {
+			odp_packet_t pkt = pkt_tbl[i];
+			odph_ethhdr_t *eth;
+
+			if (odp_unlikely(!odp_packet_has_eth(pkt))) {
+				odp_packet_free(pkt);
+				continue;
+			}
+			eth = odp_packet_l2_ptr(pkt, NULL);
+
+			if (echo_hdr(pkt) == NULL) {
+				odp_packet_free(pkt);
+				continue;
+			}
+
+			/* Swap MAC addresses */
+			eth->dst = eth->src;
+			eth->src = global.src;
+
+			tx_tbl[num_rx++] = pkt;
+		}
+		sent = odp_pktout_send(global.pktout, tx_tbl, num_rx);
+		if (odp_unlikely(sent < 0))
+			sent = 0;
+		tx_drops = num_rx - sent;
+		if (odp_unlikely(tx_drops))
+			odp_packet_free_multi(&tx_tbl[sent], tx_drops);
+	}
+	return 0;
+}
+
+/**
+ * Echo packet transmitter thread
+ */
+static int run_transmitter(void *arg ODP_UNUSED)
+{
+	for (;;) {
+		odp_packet_t pkt;
+		int sent;
+
+		pkt = odp_packet_alloc(global.pool, ECHO_PACKET_LEN);
+		if (pkt == ODP_PACKET_INVALID)
+			continue;
+
+		if (echo_packet_init(pkt)) {
+			odp_packet_free(pkt);
+			continue;
+		}
+
+		sent = odp_pktout_send(global.pktout, &pkt, 1);
+		if (odp_unlikely(sent != 1))
+			odp_packet_free(pkt);
+
+		odp_time_wait_ns(ECHO_TX_INTERVAL);
+	}
+	return 0;
+}
+
+/**
+ * Echo packet receiver thread
+ */
+static int run_receiver(void *arg ODP_UNUSED)
+{
+	odp_packet_t pkt_tbl[global.appl.burst_size];
+	int pkts, i;
+
+	for (;;) {
+		uint64_t ts;
+
+		pkts = odp_pktin_recv_tmo(global.pktin, pkt_tbl,
+					  global.appl.burst_size,
+					  global.appl.wait);
+
+		if (odp_unlikely(pkts <= 0))
+			continue;
+
+		ts = odp_time_to_ns(odp_time_global());
+
+		for (i = 0; i < pkts; i++) {
+			odp_packet_t pkt = pkt_tbl[i];
+			odph_ethhdr_t *eth;
+			odph_ethaddr_t *src;
+			echo_hdr_t *echo;
+			double delay;
+
+			if (odp_unlikely(!odp_packet_has_eth(pkt)))
+				continue;
+			eth = odp_packet_l2_ptr(pkt, NULL);
+
+			echo = echo_hdr(pkt);
+			if (!echo)
+				continue;
+
+			delay = (double)(ts - echo->ts) / ODP_TIME_MSEC_IN_NS;
+			src = &eth->src;
+
+			printf("%" PRIu32 " bytes from "
+			       "%02x:%02x:%02x:%02x:%02x:%02x: seq=%" PRIu32 " "
+			       "time=%0.3f ms\n", odp_packet_len(pkt),
+			       src->addr[0], src->addr[1], src->addr[2],
+			       src->addr[3], src->addr[4], src->addr[5],
+			       echo->seq, delay);
+		}
+		odp_packet_free_multi(pkt_tbl, pkts);
+	}
+	return 0;
+}
+
+/**
+ * Print usage information
+ */
+static void usage(char *progname)
+{
+	printf("\n"
+	       "OpenDataPlane echo example application.\n"
+	       "\n"
+	       "Usage: %s OPTIONS\n"
+	       "  E.g. host1$ %s -m 0 -i eth0 -d 02:01:02:03:04:05\n"
+	       "       host2$ %s -m 1 -i eth1\n\n"
+	       " In the above example,\n"
+	       " host1 sends echo packets from eth0 to the MAC address of\n"
+	       " host2's eth1 and waits for responses. Host2 waits for echo\n"
+	       " packets from eth1 and resends them back to the sender.\n"
+	       "\n"
+	       "Mandatory OPTIONS:\n"
+	       "  -i, --interface   Eth interface\n"
+	       "  -d, --destination Destination MAC address (required in transmitter mode)\n"
+	       "\n"
+	       "Optional OPTIONS:\n"
+	       "  -m, --mode        0: Transmitter (default)\n"
+	       "                    1: Responder\n"
+	       "  -n, --no-wait     Busy wait new packets\n"
+	       "  -b, --burst       Receive burst size\n"
+	       "  -h, --help        Display help and exit.\n\n"
+	       "\n", NO_PATH(progname), NO_PATH(progname), NO_PATH(progname));
+}
+
+/**
+ * Parse and store the command line arguments
+ *
+ * @param argc       argument count
+ * @param argv[]     argument vector
+ * @param appl_args  Store application arguments here
+ */
+static void parse_args(int argc, char *argv[], appl_args_t *appl_args)
+{
+	int opt;
+	int long_index;
+	size_t len;
+	int i;
+	odp_bool_t mac_set = 0;
+	static const char *shortopts =  "+m:i:d:b:nh";
+	static const struct option longopts[] = {
+		{"mode", required_argument, NULL, 'm'},
+		{"interface", required_argument, NULL, 'i'},
+		{"destination", required_argument, NULL, 'd'},
+		{"burst", required_argument, NULL, 'b'},
+		{"no-wait", no_argument, NULL, 'n'},
+		{"help", no_argument, NULL, 'h'},
+		{NULL, 0, NULL, 0}
+	};
+
+	appl_args->if_str = NULL;
+	appl_args->burst_size = 1;
+	appl_args->wait = ODP_PKTIN_WAIT;
+
+	/* let helper collect its own arguments (e.g. --odph_proc) */
+	odph_parse_options(argc, argv, shortopts, longopts);
+
+	opterr = 0; /* do not issue errors on helper options */
+
+	while (1) {
+		opt = getopt_long(argc, argv, shortopts, longopts, &long_index);
+
+		if (opt == -1)
+			break;	/* No more options */
+
+		switch (opt) {
+		case 'm':
+			i = atoi(optarg);
+			if (i == 1)
+				appl_args->mode = RESPOND;
+			else
+				appl_args->mode = TRANSMIT;
+			break;
+		case 'i':
+			len = strlen(optarg);
+			if (len == 0) {
+				usage(argv[0]);
+				exit(EXIT_FAILURE);
+			}
+			len += 1;	/* add room for '\0' */
+
+			appl_args->if_str = malloc(len);
+			if (appl_args->if_str == NULL) {
+				usage(argv[0]);
+				exit(EXIT_FAILURE);
+			}
+			strcpy(appl_args->if_str, optarg);
+			break;
+		case 'd':
+			if (odph_eth_addr_parse(&appl_args->dst, optarg)) {
+				usage(argv[0]);
+				exit(EXIT_FAILURE);
+			}
+			mac_set = 1;
+
+			break;
+		case 'b':
+			i = atoi(optarg);
+			if (i < 1)
+				appl_args->burst_size = 1;
+			else
+				appl_args->burst_size = i;
+			break;
+		case 'n':
+			appl_args->wait = ODP_PKTIN_NO_WAIT;
+			break;
+		case 'h':
+			usage(argv[0]);
+			exit(EXIT_SUCCESS);
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (appl_args->if_str == NULL ||
+	    (appl_args->mode == TRANSMIT && !mac_set)) {
+		usage(argv[0]);
+		exit(EXIT_FAILURE);
+	}
+
+	optind = 1;		/* reset 'extern optind' from the getopt lib */
+}
+
+int main(int argc, char **argv)
+{
+	odph_odpthread_t thread_tbl[MAX_WORKERS];
+	odp_pool_param_t params;
+	odp_cpumask_t cpumask;
+	odp_instance_t instance;
+	int i;
+	int cpu;
+	int num_workers;
+	int req_workers;
+
+	/* Initialize ODP */
+	if (odp_init_global(&instance, NULL, NULL)) {
+		printf("Error: ODP global init failed.\n");
+		exit(1);
+	}
+
+	if (odp_init_local(instance, ODP_THREAD_CONTROL)) {
+		printf("Error: ODP local init failed.\n");
+		exit(1);
+	}
+
+	/* Parse and store the application arguments */
+	parse_args(argc, argv, &global.appl);
+
+	/* Create packet pool */
+	odp_pool_param_init(&params);
+	params.pkt.seg_len = POOL_SEG_LEN;
+	params.pkt.len     = POOL_SEG_LEN;
+	params.pkt.num     = POOL_NUM_PKT;
+	params.type        = ODP_POOL_PACKET;
+
+	global.pool = odp_pool_create("packet pool", &params);
+
+	if (global.pool == ODP_POOL_INVALID) {
+		printf("Error: packet pool create failed.\n");
+		exit(1);
+	}
+
+	/* Create and configure pktio device */
+	global.iface = create_pktio(global.appl.if_str, global.pool,
+				    &global.pktin, &global.pktout);
+
+	/* Save interface ethernet address */
+	if (odp_pktio_mac_addr(global.iface, &global.src,
+			       ODPH_ETHADDR_LEN) != ODPH_ETHADDR_LEN) {
+		printf("Error: interface ethernet address unknown\n");
+		exit(EXIT_FAILURE);
+	}
+
+	if (odp_pktio_start(global.iface)) {
+		printf("Error: unable to start input interface\n");
+		exit(1);
+	}
+
+	/* Create and bind worker threads */
+	req_workers = global.appl.mode == TRANSMIT ? 2 : 1;
+	num_workers = odp_cpumask_default_worker(&cpumask, MAX_WORKERS);
+
+	if (num_workers < req_workers) {
+		printf("Error: not enough workers (%d available, %d required)\n",
+		       num_workers, req_workers);
+		exit(1);
+	}
+
+	cpu = odp_cpumask_first(&cpumask);
+	for (i = 0; i < req_workers; i++) {
+		odp_cpumask_t thd_mask;
+		odph_odpthread_params_t thr_params;
+
+		memset(&thr_params, 0, sizeof(thr_params));
+
+		if (global.appl.mode == RESPOND)
+			thr_params.start = run_responder;
+		else
+			thr_params.start = i % 2 ? run_transmitter :
+					run_receiver;
+
+		thr_params.arg      = NULL;
+		thr_params.thr_type = ODP_THREAD_WORKER;
+		thr_params.instance = instance;
+
+		odp_cpumask_zero(&thd_mask);
+		odp_cpumask_set(&thd_mask, cpu);
+		odph_odpthreads_create(&thread_tbl[i], &thd_mask,
+				       &thr_params);
+		cpu = odp_cpumask_next(&cpumask, cpu);
+	}
+
+	odph_odpthreads_join(thread_tbl);
+
+	return 0;
+}
diff --git a/example/m4/configure.m4 b/example/m4/configure.m4
index 9731d81..d54ea72 100644
--- a/example/m4/configure.m4
+++ b/example/m4/configure.m4
@@ -19,4 +19,5 @@ AC_CONFIG_FILES([example/classifier/Makefile
 		 example/timer/Makefile
 		 example/traffic_mgmt/Makefile
 		 example/l2fwd_simple/Makefile
-		 example/switch/Makefile])
+		 example/switch/Makefile
+		 example/echo/Makefile])
